From 9991b6d88bd15796ed00c11f3e8eee04fa2cdc3e Mon Sep 17 00:00:00 2001 From: Dylan Date: Mon, 23 Feb 2026 21:39:49 +0000 Subject: [PATCH 01/74] [ENG-2667] Fix copybara no-op failures for go/ package replacements (#3980) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Fix broken CLI releases (v1.16.0–v1.16.2) caused by copybara failing on historical commits in ITERATIVE mode - Wrap all `go/*` import replacements in `core.transform` with `noop_behavior = "IGNORE_NOOP"` so old commits (predating the `go/` directory restructuring) don't cause migration failures - Apply the same fix to the `terraform-provider` workflow which has the same issue ## Linear Ticket - **Ticket**: [ENG-2667](https://linear.app/overmind/issue/ENG-2667/copybara-cli-releases-are-broken) — copybara / CLI releases are broken ## Changes The copybara `default` and `terraform-provider` workflows use ITERATIVE mode, which replays every workspace commit against the destination repo. Commits predating the `go/` directory restructuring (Feb 16, ENG-2422) don't contain import paths like `github.com/overmindtech/workspace/go/auth` — so the replacement rules for those paths were no-ops on old commits, causing copybara to fail with exit code 2. The fix groups all `go/*` replacements (`go/auth`, `go/discovery`, `go/sdp-go`, `go/sdpcache`, `go/tracing`, `go/logging`) into a single `core.transform` block with `noop_behavior = "IGNORE_NOOP"`. This matches the existing pattern already in place for `go/logging`, and is safe because the workspace module enforces correct import paths — any file that compiles uses the new `go/` paths. Replacements for paths that have always existed at the same location (`aws-source`, `sources`, `stdlib-source`, `k8s-source`, `cli`) are left as-is with the default fail-on-noop behaviour. Made with [Cursor](https://cursor.com) --- > [!NOTE] > **Low Risk** > Config-only change to Copybara transformations that only relaxes behavior for missing historical import paths; it doesn’t alter runtime product code. > > **Overview** > Prevents Copybara `ITERATIVE` migrations from failing on older workspace commits by wrapping all `go/*` import-path `core.replace` rules in a single `core.transform(..., noop_behavior = "IGNORE_NOOP")`. > > Applies this both to the `default` (CLI) workflow and the `terraform-provider` workflow, while keeping non-`go/*` replacements as strict (fail-on-noop). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 3ce476f87712de6b3dbb6e59a878222e3d231a19. Configure [here](https://cursor.com/dashboard?tab=bugbot). Co-authored-by: Cursor GitOrigin-RevId: 7b0fa3794a1c65dc9b6d70160c4e703aa39f2d85 --- go.mod | 193 ++++++++++++++- go.sum | 725 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 912 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 94be0a7f..120b4e1b 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 // 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 @@ -39,6 +39,8 @@ 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 @@ -55,6 +57,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.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 @@ -80,50 +88,84 @@ 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 // 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.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/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.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 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 @@ -153,11 +195,16 @@ 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 @@ -171,15 +218,23 @@ 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 @@ -195,6 +250,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 @@ -202,37 +265,65 @@ 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 @@ -242,6 +333,7 @@ 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 @@ -252,20 +344,38 @@ 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/lithammer/fuzzysearch v1.1.8 // 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/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 @@ -277,41 +387,104 @@ 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 @@ -321,15 +494,23 @@ 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 // indirect + 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 15504935..1b0cecf6 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.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= @@ -22,6 +33,8 @@ 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= @@ -30,6 +43,7 @@ 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= @@ -40,6 +54,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= @@ -58,6 +73,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= @@ -70,6 +87,8 @@ 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= @@ -78,8 +97,13 @@ 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= @@ -122,8 +146,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.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= @@ -147,10 +173,29 @@ 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= @@ -161,6 +206,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= @@ -170,7 +221,11 @@ 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= @@ -241,6 +296,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= @@ -257,18 +314,50 @@ 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= @@ -289,15 +378,32 @@ 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= @@ -305,22 +411,44 @@ 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= @@ -328,12 +456,23 @@ 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= @@ -342,6 +481,8 @@ 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= @@ -349,29 +490,66 @@ 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= @@ -380,17 +558,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= @@ -399,6 +603,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= @@ -409,8 +615,14 @@ 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= @@ -434,6 +646,10 @@ 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= @@ -466,8 +682,24 @@ 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= @@ -476,21 +708,39 @@ 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= @@ -500,12 +750,36 @@ 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= @@ -519,6 +793,14 @@ 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= @@ -527,10 +809,14 @@ 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= @@ -545,6 +831,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= @@ -563,16 +851,34 @@ 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= @@ -587,13 +893,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= @@ -603,24 +954,78 @@ 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= @@ -639,10 +1044,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= @@ -655,10 +1062,61 @@ 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= @@ -670,6 +1128,12 @@ 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= @@ -683,12 +1147,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= @@ -697,8 +1170,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= @@ -713,6 +1202,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/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8= @@ -739,102 +1230,279 @@ 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= @@ -842,34 +1510,91 @@ 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= From 0e5794aad529f2b70154bf3f2690a93fead477b4 Mon Sep 17 00:00:00 2001 From: Elliot Waddington Date: Tue, 24 Feb 2026 11:49:04 +0100 Subject: [PATCH 02/74] Blast propagation documentation (#3979) Remove all references to "blast propagation" from documentation and cursor rules, as blast radius is now AI-driven only. Previously, adapters required hardcoded blast radius information, but with the new AI-driven approach, these references are obsolete and have been removed across the codebase to reflect the updated system. --- Linear Issue: [ENG-2474](https://linear.app/overmind/issue/ENG-2474/docs-update-all-documentation-and-cursor-rules-to-remove-blast)

Open in Web Open in Cursor 

--- > [!NOTE] > **Low Risk** > Primarily documentation/comment updates with a small, straightforward helper signature change; minimal risk beyond potential compile breakage where old `AppendURILinks` args were still used. > > **Overview** > Removes remaining *blast propagation* terminology from docs and Cursor rules, standardizing dynamic adapter linking language around `linkRules` and AI-driven blast radius. > > Cleans up a few code comments and call sites to match the new model, including simplifying Azure `AppendURILinks` usage (dropping blast in/out params) and updating snapshot edge hydration comments to no longer mention blast propagation. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0f43ba4813a82ccb38f2c59789dc8b28ac0cd404. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Cursor Agent GitOrigin-RevId: 1bbe4ea80eca56a3bb44055cd045170d1d28cd22 --- sources/azure/manual/README.md | 4 ++-- .../manual/compute-gallery-application-version.go | 2 +- sources/azure/manual/compute-gallery-image.go | 6 +++--- sources/azure/manual/compute-gallery.go | 4 ++-- sources/azure/manual/compute-shared-gallery-image.go | 4 ++-- sources/azure/manual/links_helpers.go | 2 -- sources/gcp/dynamic/README.md | 8 ++++---- .../ai-platform-batch-prediction-job_test.go | 2 +- .../adapters/storage-transfer-transfer-job.go | 2 +- sources/gcp/dynamic/ai-tools/README.md | 12 ++++++------ sources/gcp/manual/README.md | 6 +++--- sources/snapshot/adapters/index.go | 3 +-- 12 files changed, 26 insertions(+), 29 deletions(-) diff --git a/sources/azure/manual/README.md b/sources/azure/manual/README.md index ee64ddcc..324b0035 100644 --- a/sources/azure/manual/README.md +++ b/sources/azure/manual/README.md @@ -54,7 +54,7 @@ Refer to the [cursor rules](.cursor/rules/azure-manual-adapter-creation.mdc) for 3. **Handle Complex Resource Linking**: - Parse Azure resource IDs to extract resource names and types - Extract resource identifiers from Azure resource manager format - - Create appropriate linked item queries with correct blast propagation + - Create appropriate linked item queries 4. **Include Comprehensive Tests**: - Unit tests for all methods @@ -85,7 +85,7 @@ When reviewing PRs for manual adapters, ensure: ### ✅ Linked Item Queries - [ ] Example values in tests match actual Azure resource formats - [ ] Scopes for linked item queries are correct (verify with linked resource documentation) -- [ ] Blast propagation rules are appropriate for resource relationships +- [ ] Linked item queries are appropriately defined - [ ] All possible resource references are handled (no missing cases) ### ✅ Documentation and References diff --git a/sources/azure/manual/compute-gallery-application-version.go b/sources/azure/manual/compute-gallery-application-version.go index e5773d47..abee7c96 100644 --- a/sources/azure/manual/compute-gallery-application-version.go +++ b/sources/azure/manual/compute-gallery-application-version.go @@ -204,7 +204,7 @@ func (c computeGalleryApplicationVersionWrapper) azureGalleryApplicationVersionT if link == "" || (!strings.HasPrefix(link, "http://") && !strings.HasPrefix(link, "https://")) { return } - AppendURILinks(&linkedItemQueries, link, linkedDNSHostnames, seenIPs, true, true) + AppendURILinks(&linkedItemQueries, link, linkedDNSHostnames, seenIPs) 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 index 8fdfd581..2da9df24 100644 --- a/sources/azure/manual/compute-gallery-image.go +++ b/sources/azure/manual/compute-gallery-image.go @@ -173,13 +173,13 @@ func (c computeGalleryImageWrapper) azureGalleryImageToSDPItem( seenIPs := make(map[string]struct{}) if galleryImage.Properties != nil { if galleryImage.Properties.Eula != nil { - AppendURILinks(&linkedItemQueries, *galleryImage.Properties.Eula, linkedDNSHostnames, seenIPs, true, false) + AppendURILinks(&linkedItemQueries, *galleryImage.Properties.Eula, linkedDNSHostnames, seenIPs) } if galleryImage.Properties.PrivacyStatementURI != nil { - AppendURILinks(&linkedItemQueries, *galleryImage.Properties.PrivacyStatementURI, linkedDNSHostnames, seenIPs, true, false) + AppendURILinks(&linkedItemQueries, *galleryImage.Properties.PrivacyStatementURI, linkedDNSHostnames, seenIPs) } if galleryImage.Properties.ReleaseNoteURI != nil { - AppendURILinks(&linkedItemQueries, *galleryImage.Properties.ReleaseNoteURI, linkedDNSHostnames, seenIPs, true, false) + AppendURILinks(&linkedItemQueries, *galleryImage.Properties.ReleaseNoteURI, linkedDNSHostnames, seenIPs) } } diff --git a/sources/azure/manual/compute-gallery.go b/sources/azure/manual/compute-gallery.go index 48f3fc40..8e7c9a75 100644 --- a/sources/azure/manual/compute-gallery.go +++ b/sources/azure/manual/compute-gallery.go @@ -140,10 +140,10 @@ func (c computeGalleryWrapper) azureGalleryToSDPItem(gallery *armcompute.Gallery 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) + AppendURILinks(&linkedItemQueries, *info.PublisherURI, linkedDNSHostnames, seenIPs) } if info.Eula != nil { - AppendURILinks(&linkedItemQueries, *info.Eula, linkedDNSHostnames, seenIPs, true, false) + AppendURILinks(&linkedItemQueries, *info.Eula, linkedDNSHostnames, seenIPs) } } diff --git a/sources/azure/manual/compute-shared-gallery-image.go b/sources/azure/manual/compute-shared-gallery-image.go index 37ba6c70..2d736ff8 100644 --- a/sources/azure/manual/compute-shared-gallery-image.go +++ b/sources/azure/manual/compute-shared-gallery-image.go @@ -174,10 +174,10 @@ func (c computeSharedGalleryImageWrapper) azureSharedGalleryImageToSDPItem( seenIPs := make(map[string]struct{}) if image.Properties != nil { if image.Properties.Eula != nil { - AppendURILinks(&linkedItemQueries, *image.Properties.Eula, linkedDNSHostnames, seenIPs, true, false) + AppendURILinks(&linkedItemQueries, *image.Properties.Eula, linkedDNSHostnames, seenIPs) } if image.Properties.PrivacyStatementURI != nil { - AppendURILinks(&linkedItemQueries, *image.Properties.PrivacyStatementURI, linkedDNSHostnames, seenIPs, true, false) + AppendURILinks(&linkedItemQueries, *image.Properties.PrivacyStatementURI, linkedDNSHostnames, seenIPs) } } diff --git a/sources/azure/manual/links_helpers.go b/sources/azure/manual/links_helpers.go index 46302efc..eb7549c1 100644 --- a/sources/azure/manual/links_helpers.go +++ b/sources/azure/manual/links_helpers.go @@ -33,13 +33,11 @@ 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 diff --git a/sources/gcp/dynamic/README.md b/sources/gcp/dynamic/README.md index 4438728f..6da085b6 100644 --- a/sources/gcp/dynamic/README.md +++ b/sources/gcp/dynamic/README.md @@ -54,8 +54,8 @@ The complete flow from making a GET request to creating an SDP adapter follows t 1. **Adapter Definition**: Define the adapter metadata in the adapter file (see [dynamic-adapter-creation.mdc](adapters/.cursor/rules/dynamic-adapter-creation.mdc)) 2. **Adapter Creation**: Framework creates the appropriate adapter type based on metadata configuration 3. **GET Request Processing**: Validate scope, check cache, construct URL, make HTTP request, convert to SDP item -4. **External Response to SDP Conversion**: Extract attributes, apply blast propagation rules, generate linked item queries -5. **Unit Test Coverage**: Test GET functionality and static tests for blast propagation +4. **External Response to SDP Conversion**: Extract attributes, apply link rules, generate linked item queries +5. **Unit Test Coverage**: Test GET functionality and static tests for link rules For detailed implementation patterns and code examples, refer to the [dynamic adapter creation rules](adapters/.cursor/rules/dynamic-adapter-creation.mdc). @@ -89,13 +89,13 @@ It is highly recommended to use Cursor for creating adapters. There are comprehe ### Adapter Validation 1. **Terraform Mappings GET/Search**: Check from Terraform registry that the mappings are correct -2. **Blast Propagations**: Verify they are comprehensive and attribute values follow standards +2. **Link Rules**: Verify they are comprehensive and attribute values follow standards 3. **Item Selector**: If the item identifier in the API response is something other than `name`, define it properly 4. **Unique Attribute Keys**: Investigate the GET endpoint format and ensure it's correct ### Test Completeness -1. **Blast Propagation/Linked Item Queries**: Verify they work as expected +1. **Linked Item Queries**: Verify they work as expected 2. **Unique Attribute**: Ensure it matches the GET call response 3. **Terraform Mapping for Search**: Confirm it exists if search is supported 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 52438dcc..c389490f 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 @@ -8,7 +8,7 @@ package adapters_test // implementation: // 1. Protobuf serializes field names to snake_case (e.g., "batch_prediction_jobs") while the // adapter configuration expects camelCase (e.g., "batchPredictionJobs"), affecting list operations -// 2. Blast propagation paths in the adapter expect JSON field names but get protobuf field names, +// 2. Link rule paths in the adapter expect JSON field names but get protobuf field names, // limiting automatic link generation for nested fields like GCS sources and KMS keys // // These limitations don't affect the core functionality testing but are noted for future improvements. diff --git a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go index d13eeae9..bc60b3bb 100644 --- a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go +++ b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job.go @@ -108,7 +108,7 @@ var _ = registerableAdapter{ }, // 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' - // https://linear.app/overmind/issue/ENG-1348/investigate-supporting-multiple-items-in-blast-propagations + // https://linear.app/overmind/issue/ENG-1348 // Required. Specifies a unique name of the resource such as AWS SQS ARN in the form 'arn:aws:sqs:region:account_id:queue_name', // or Pub/Sub subscription resource name in the form 'projects/{project}/subscriptions/{sub}'. "eventStream.name": { diff --git a/sources/gcp/dynamic/ai-tools/README.md b/sources/gcp/dynamic/ai-tools/README.md index cd146d97..dc07f1d1 100644 --- a/sources/gcp/dynamic/ai-tools/README.md +++ b/sources/gcp/dynamic/ai-tools/README.md @@ -98,7 +98,7 @@ go run generate-test-ticket-cmd/main.go -v compute-global-address ### What it does 1. **Extract adapter information** from the adapter file in `../adapters/` 2. **Determine protobuf types** based on adapter name patterns -3. **Extract blast propagation** configuration from the adapter +3. **Extract link rules** configuration from the adapter 4. **Generate a Linear URL** with basic fields pre-filled: - Title: "Write unit test for {adapter-name} dynamic adapter" - Assignee: Cursor Agent @@ -115,7 +115,7 @@ The tool generates a Linear URL with basic fields and copies the description to ### Requirements - Must be run from the `prompter` directory - Adapter file must exist in `../adapters/` -- Adapter file must contain valid SDP item type and blast propagation configuration +- Adapter file must contain valid SDP item type and link rules configuration - Go 1.19+ required ## Integration with Cursor Agents @@ -163,7 +163,7 @@ When a Cursor agent picks up the ticket: 2. Follow the comprehensive testing patterns 3. Create the test file with proper structure 4. Include all required test cases (Get, List, Search if supported) -5. Add proper blast propagation tests +5. Add proper link rules tests ## Example Ticket Content @@ -177,7 +177,7 @@ For `compute-global-forwarding-rule`: - **API Endpoints**: - GET: `https://compute.googleapis.com/compute/v1/projects/{project}/global/forwardingRules/{forwardingRule}` - LIST: `https://compute.googleapis.com/compute/v1/projects/{project}/global/forwardingRules` -- **Blast Propagation**: network (InOnly), subnetwork (InOnly), IPAddress (BothWays), backendService (BothWays) +- **Link Rules**: network (InOnly), subnetwork (InOnly), IPAddress (BothWays), backendService (BothWays) ## Benefits @@ -223,7 +223,7 @@ When you just need tests for an existing adapter: The `../adapters/.cursor/rules/dynamic-adapter-creation.md` file ensures that: - Proper adapter structure and patterns are followed - Correct SDP item types and metadata are defined -- Appropriate blast propagation is configured +- Appropriate link rules are configured - Terraform mappings are included when applicable - IAM permissions are properly defined @@ -233,7 +233,7 @@ The `../adapters/.cursor/rules/dynamic-adapter-testing.md` file ensures that: - Proper imports are included - Correct protobuf types are used - Comprehensive test coverage is provided -- Static tests with blast propagation are included +- Static tests with link rules are included - Common mistakes are avoided This ensures consistent, high-quality implementations and unit tests for all dynamic adapters. diff --git a/sources/gcp/manual/README.md b/sources/gcp/manual/README.md index dc2e8b09..410e0c9d 100644 --- a/sources/gcp/manual/README.md +++ b/sources/gcp/manual/README.md @@ -58,7 +58,7 @@ Refer to the [cursor rules](.cursor/rules/gcp-manual-adapter-creation.mdc) for c 3. **Handle Complex Resource Linking**: - Parse non-standard API response formats - Extract resource identifiers from various formats - - Create appropriate linked item queries with correct blast propagation + - Create appropriate linked item queries 4. **Include Comprehensive Tests**: - Unit tests for all methods @@ -89,12 +89,12 @@ When reviewing PRs for manual adapters, ensure: ### ✅ Linked Item Queries - [ ] Example values in tests match actual GCP resource formats - [ ] Scopes for linked item queries are correct (verify with linked resource documentation) -- [ ] Blast propagation rules are appropriate for resource relationships +- [ ] Linked item queries are appropriately defined - [ ] All possible resource references are handled (no missing cases) ### ✅ Documentation and References - [ ] GCP API documentation URLs are included in comments -- [ ] Resource relationship explanations are documented +- [ ] Resource linking explanations are documented - [ ] Complex parsing logic is well-commented - [ ] Official GCP reference links are provided for linked resources diff --git a/sources/snapshot/adapters/index.go b/sources/snapshot/adapters/index.go index fcbd92d0..4808d833 100644 --- a/sources/snapshot/adapters/index.go +++ b/sources/snapshot/adapters/index.go @@ -69,8 +69,7 @@ func NewSnapshotIndex(snapshot *sdp.Snapshot) (*SnapshotIndex, error) { // 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. +// to edge.To. 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. From 2d1e9b715f90e576ea9140b321a8c6515bb70f7d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:30:06 +0000 Subject: [PATCH 03/74] chore(deps): update golang docker tag to v1.26 (#3869) 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 | |---|---|---|---| | golang | stage | minor | `1.25-alpine` → `1.26-alpine` | | golang | | minor | `1.25-bookworm` → `1.26-bookworm` | --- > [!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**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about these updates 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). --- > [!NOTE] > **Medium Risk** > Toolchain and container base image upgrades can cause subtle build/test failures or behavior changes across all Go services. Renovate/CI changes are straightforward but affect automation and linting consistency. > > **Overview** > **Upgrades the Go toolchain baseline to 1.26** across the repo: `go.mod` now targets Go `1.26.0`, the devcontainer base image moves to `dev-1.26-bookworm` (with cache keys updated), and all build/package Dockerfiles plus gateway compose dev images switch from `golang:1.25-*` to `golang:1.26-*`. > > Also bumps `golangci-lint` from `v2.7.2` to `v2.9.0` in both the devcontainer and CI, and extends `renovate.json` with regex managers so Renovate can track/update the Go devcontainer tag, golangci-lint version, and Go-cache key versions. Azure manual tests are adjusted to stop asserting `ExpectedBlastPropagation` in `QueryTests` fixtures. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f6893ea8e3f8788f4b7f363cf1219429dd6886d4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: David Schmitt Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> GitOrigin-RevId: 0ab86ed3afecb3d30e7f7e090f2d8493ea52a55c --- aws-source/build/package/Dockerfile | 2 +- go.mod | 2 +- k8s-source/build/package/Dockerfile | 2 +- sources/azure/build/package/Dockerfile | 2 +- sources/gcp/build/package/Dockerfile | 2 +- stdlib-source/build/package/Dockerfile | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/aws-source/build/package/Dockerfile b/aws-source/build/package/Dockerfile index 5ea83d03..e6cd5153 100644 --- a/aws-source/build/package/Dockerfile +++ b/aws-source/build/package/Dockerfile @@ -1,5 +1,5 @@ # Build the source binary -FROM golang:1.25-alpine AS builder +FROM golang:1.26-alpine AS builder ARG TARGETOS ARG TARGETARCH ARG BUILD_VERSION diff --git a/go.mod b/go.mod index 120b4e1b..95ab4cf0 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/overmindtech/cli -go 1.25.1 +go 1.26.0 replace github.com/anthropics/anthropic-sdk-go => github.com/anthropics/anthropic-sdk-go v0.2.0-alpha.4 diff --git a/k8s-source/build/package/Dockerfile b/k8s-source/build/package/Dockerfile index c9f3c6a8..f579cf84 100644 --- a/k8s-source/build/package/Dockerfile +++ b/k8s-source/build/package/Dockerfile @@ -1,5 +1,5 @@ # Build the source binary -FROM golang:1.25-alpine AS builder +FROM golang:1.26-alpine AS builder ARG TARGETOS ARG TARGETARCH ARG BUILD_VERSION diff --git a/sources/azure/build/package/Dockerfile b/sources/azure/build/package/Dockerfile index 3b607dc5..1fc2d532 100644 --- a/sources/azure/build/package/Dockerfile +++ b/sources/azure/build/package/Dockerfile @@ -1,5 +1,5 @@ # Build the source binary -FROM golang:1.25-alpine AS builder +FROM golang:1.26-alpine AS builder ARG TARGETOS ARG TARGETARCH ARG BUILD_VERSION diff --git a/sources/gcp/build/package/Dockerfile b/sources/gcp/build/package/Dockerfile index 853a571a..bfd42f67 100644 --- a/sources/gcp/build/package/Dockerfile +++ b/sources/gcp/build/package/Dockerfile @@ -1,5 +1,5 @@ # Build the source binary -FROM golang:1.25-alpine AS builder +FROM golang:1.26-alpine AS builder ARG TARGETOS ARG TARGETARCH ARG BUILD_VERSION diff --git a/stdlib-source/build/package/Dockerfile b/stdlib-source/build/package/Dockerfile index deb0f149..4c8d1112 100644 --- a/stdlib-source/build/package/Dockerfile +++ b/stdlib-source/build/package/Dockerfile @@ -1,5 +1,5 @@ # Build the source binary -FROM golang:1.25-alpine AS builder +FROM golang:1.26-alpine AS builder ARG TARGETOS ARG TARGETARCH ARG BUILD_VERSION From 588ccf191cd33685aec50191c422c78035d06420 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:50:28 +0000 Subject: [PATCH 04/74] Eng 2435 create networksubnet adapter (#3978) image > [!NOTE] > **Medium Risk** > Adds new discovery surface area and link generation across many Azure network relationships; risk is mainly incorrect type inference or linking causing noisy/missing graph edges rather than security impact. > > **Overview** > Adds a new Azure `NetworkSubnet` searchable adapter (and `SubnetsClient` wrapper) and wires it into `manual/adapters.go` so subnets can be discovered per-virtual-network, including rich linked-item query generation (NSG/route table/NAT gateway/private endpoints/NICs/app gateways and resource navigation/service association links). > > Introduces `shared.ItemTypeFromLinkedResourceID` (with tests) to infer `azure-{api}-{resource}` types from Azure resource IDs, adds missing network item types (`ServiceEndpointPolicy`, `IpAllocation`), and registers subnet path extraction in `GetResourceIDPathKeys`. Also updates compute gallery linking/tests to include gallery applications as child resources. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 375ce66214732222c20756b7c7c7801079693de4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: be036b8890b6aaa841bb1134525d40bdd9bd12de --- sources/azure/clients/subnets-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + sources/azure/manual/compute-gallery.go | 10 + sources/azure/manual/compute-gallery_test.go | 6 + sources/azure/manual/network-subnet.go | 452 ++++++++++++++++++ sources/azure/manual/network-subnet_test.go | 278 +++++++++++ sources/azure/shared/item-types.go | 2 + .../azure/shared/mocks/mock_subnets_client.go | 72 +++ sources/azure/shared/models.go | 2 + sources/azure/shared/resource_id_item_type.go | 98 ++++ .../shared/resource_id_item_type_test.go | 116 +++++ sources/azure/shared/utils.go | 1 + 12 files changed, 1082 insertions(+) create mode 100644 sources/azure/clients/subnets-client.go create mode 100644 sources/azure/manual/network-subnet.go create mode 100644 sources/azure/manual/network-subnet_test.go create mode 100644 sources/azure/shared/mocks/mock_subnets_client.go create mode 100644 sources/azure/shared/resource_id_item_type.go create mode 100644 sources/azure/shared/resource_id_item_type_test.go diff --git a/sources/azure/clients/subnets-client.go b/sources/azure/clients/subnets-client.go new file mode 100644 index 00000000..1f3dfce0 --- /dev/null +++ b/sources/azure/clients/subnets-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" +) + +//go:generate mockgen -destination=../shared/mocks/mock_subnets_client.go -package=mocks -source=subnets-client.go + +// SubnetsPager is a type alias for the generic Pager interface with subnet list response type. +type SubnetsPager = Pager[armnetwork.SubnetsClientListResponse] + +// SubnetsClient is an interface for interacting with Azure virtual network subnets. +type SubnetsClient interface { + Get(ctx context.Context, resourceGroupName string, virtualNetworkName string, subnetName string, options *armnetwork.SubnetsClientGetOptions) (armnetwork.SubnetsClientGetResponse, error) + NewListPager(resourceGroupName string, virtualNetworkName string, options *armnetwork.SubnetsClientListOptions) SubnetsPager +} + +type subnetsClientAdapter struct { + client *armnetwork.SubnetsClient +} + +func (a *subnetsClientAdapter) Get(ctx context.Context, resourceGroupName string, virtualNetworkName string, subnetName string, options *armnetwork.SubnetsClientGetOptions) (armnetwork.SubnetsClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, virtualNetworkName, subnetName, options) +} + +func (a *subnetsClientAdapter) NewListPager(resourceGroupName string, virtualNetworkName string, options *armnetwork.SubnetsClientListOptions) SubnetsPager { + return a.client.NewListPager(resourceGroupName, virtualNetworkName, options) +} + +// NewSubnetsClient creates a new SubnetsClient from the Azure SDK client. +func NewSubnetsClient(client *armnetwork.SubnetsClient) SubnetsClient { + return &subnetsClientAdapter{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index ba2172fa..bdf0799f 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -111,6 +111,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create virtual networks client: %w", err) } + subnetsClient, err := armnetwork.NewSubnetsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create subnets client: %w", err) + } + networkInterfacesClient, err := armnetwork.NewInterfacesClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create network interfaces client: %w", err) @@ -309,6 +314,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewVirtualNetworksClient(virtualNetworksClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewNetworkSubnet( + clients.NewSubnetsClient(subnetsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewNetworkNetworkInterface( clients.NewNetworkInterfacesClient(networkInterfacesClient), resourceGroupScopes, @@ -471,6 +480,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewStorageQueues(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewStorageTable(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkVirtualNetwork(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewNetworkSubnet(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkNetworkInterface(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewSqlDatabase(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDocumentDBDatabaseAccounts(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/compute-gallery.go b/sources/azure/manual/compute-gallery.go index 8e7c9a75..b1ba4563 100644 --- a/sources/azure/manual/compute-gallery.go +++ b/sources/azure/manual/compute-gallery.go @@ -134,6 +134,16 @@ func (c computeGalleryWrapper) azureGalleryToSDPItem(gallery *armcompute.Gallery }, }) + // Child resources: list gallery applications under this gallery (Search by gallery name) + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeGalleryApplication.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{}) diff --git a/sources/azure/manual/compute-gallery_test.go b/sources/azure/manual/compute-gallery_test.go index a6c2fde0..4ba09556 100644 --- a/sources/azure/manual/compute-gallery_test.go +++ b/sources/azure/manual/compute-gallery_test.go @@ -72,6 +72,12 @@ func TestComputeGallery(t *testing.T) { ExpectedQuery: galleryName, ExpectedScope: scope, }, + { + ExpectedType: azureshared.ComputeGalleryApplication.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: galleryName, + ExpectedScope: scope, + }, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) }) diff --git a/sources/azure/manual/network-subnet.go b/sources/azure/manual/network-subnet.go new file mode 100644 index 00000000..0cf0875d --- /dev/null +++ b/sources/azure/manual/network-subnet.go @@ -0,0 +1,452 @@ +package manual + +import ( + "context" + "errors" + "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" +) + +var NetworkSubnetLookupByUniqueAttr = shared.NewItemTypeLookup("uniqueAttr", azureshared.NetworkSubnet) + +type networkSubnetWrapper struct { + client clients.SubnetsClient + + *azureshared.MultiResourceGroupBase +} + +// NewNetworkSubnet creates a new networkSubnetWrapper instance (SearchableWrapper: child of virtual network). +func NewNetworkSubnet(client clients.SubnetsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &networkSubnetWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + azureshared.NetworkSubnet, + ), + } +} + +func (n networkSubnetWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: virtualNetworkName and subnetName", + Scope: scope, + ItemType: n.Type(), + } + } + virtualNetworkName := queryParts[0] + subnetName := queryParts[1] + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + resp, err := n.client.Get(ctx, rgScope.ResourceGroup, virtualNetworkName, subnetName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + return n.azureSubnetToSDPItem(&resp.Subnet, virtualNetworkName, subnetName, scope) +} + +func (n networkSubnetWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + NetworkVirtualNetworkLookupByName, + NetworkSubnetLookupByUniqueAttr, + } +} + +func (n networkSubnetWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: virtualNetworkName", + Scope: scope, + ItemType: n.Type(), + } + } + virtualNetworkName := queryParts[0] + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + pager := n.client.NewListPager(rgScope.ResourceGroup, virtualNetworkName, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + for _, subnet := range page.Value { + if subnet == nil || subnet.Name == nil { + continue + } + item, sdpErr := n.azureSubnetToSDPItem(subnet, virtualNetworkName, *subnet.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (n networkSubnetWrapper) 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: virtualNetworkName"), scope, n.Type())) + return + } + virtualNetworkName := queryParts[0] + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + pager := n.client.NewListPager(rgScope.ResourceGroup, virtualNetworkName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + for _, subnet := range page.Value { + if subnet == nil || subnet.Name == nil { + continue + } + item, sdpErr := n.azureSubnetToSDPItem(subnet, virtualNetworkName, *subnet.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (n networkSubnetWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + NetworkVirtualNetworkLookupByName, + }, + } +} + +func (n networkSubnetWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.NetworkVirtualNetwork: true, + azureshared.NetworkNetworkSecurityGroup: true, + azureshared.NetworkRouteTable: true, + azureshared.NetworkNatGateway: true, + azureshared.NetworkPrivateEndpoint: true, + azureshared.NetworkServiceEndpointPolicy: true, + azureshared.NetworkIpAllocation: true, + azureshared.NetworkNetworkInterface: true, + azureshared.NetworkApplicationGateway: true, + } +} + +func (n networkSubnetWrapper) azureSubnetToSDPItem(subnet *armnetwork.Subnet, virtualNetworkName, subnetName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(subnet, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(virtualNetworkName, subnetName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.NetworkSubnet.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + } + + // Link to parent Virtual Network + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkVirtualNetwork.String(), + Method: sdp.QueryMethod_GET, + Query: virtualNetworkName, + Scope: scope, + }, + }) + + // Link to Network Security Group from subnet + if subnet.Properties != nil && subnet.Properties.NetworkSecurityGroup != nil && subnet.Properties.NetworkSecurityGroup.ID != nil { + nsgID := *subnet.Properties.NetworkSecurityGroup.ID + nsgName := azureshared.ExtractResourceName(nsgID) + if nsgName != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(nsgID); extractedScope != "" { + linkScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkNetworkSecurityGroup.String(), + Method: sdp.QueryMethod_GET, + Query: nsgName, + Scope: linkScope, + }, + }) + } + } + + // Link to Route Table from subnet + if subnet.Properties != nil && subnet.Properties.RouteTable != nil && subnet.Properties.RouteTable.ID != nil { + routeTableID := *subnet.Properties.RouteTable.ID + routeTableName := azureshared.ExtractResourceName(routeTableID) + if routeTableName != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(routeTableID); extractedScope != "" { + linkScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkRouteTable.String(), + Method: sdp.QueryMethod_GET, + Query: routeTableName, + Scope: linkScope, + }, + }) + } + } + + // Link to NAT Gateway from subnet + if subnet.Properties != nil && subnet.Properties.NatGateway != nil && subnet.Properties.NatGateway.ID != nil { + natGatewayID := *subnet.Properties.NatGateway.ID + natGatewayName := azureshared.ExtractResourceName(natGatewayID) + if natGatewayName != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(natGatewayID); extractedScope != "" { + linkScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkNatGateway.String(), + Method: sdp.QueryMethod_GET, + Query: natGatewayName, + Scope: linkScope, + }, + }) + } + } + + // Link to Private Endpoints from subnet (read-only references) + if subnet.Properties != nil && subnet.Properties.PrivateEndpoints != nil { + for _, privateEndpoint := range subnet.Properties.PrivateEndpoints { + if privateEndpoint != nil && privateEndpoint.ID != nil { + privateEndpointID := *privateEndpoint.ID + privateEndpointName := azureshared.ExtractResourceName(privateEndpointID) + if privateEndpointName != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(privateEndpointID); extractedScope != "" { + linkScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPrivateEndpoint.String(), + Method: sdp.QueryMethod_GET, + Query: privateEndpointName, + Scope: linkScope, + }, + }) + } + } + } + } + + // Link to Service Endpoint Policies from subnet + if subnet.Properties != nil && subnet.Properties.ServiceEndpointPolicies != nil { + for _, policy := range subnet.Properties.ServiceEndpointPolicies { + if policy != nil && policy.ID != nil { + policyID := *policy.ID + policyName := azureshared.ExtractResourceName(policyID) + if policyName != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(policyID); extractedScope != "" { + linkScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkServiceEndpointPolicy.String(), + Method: sdp.QueryMethod_GET, + Query: policyName, + Scope: linkScope, + }, + }) + } + } + } + } + + // Link to IP Allocations from subnet (references that use this subnet) + if subnet.Properties != nil && subnet.Properties.IPAllocations != nil { + for _, ipAlloc := range subnet.Properties.IPAllocations { + if ipAlloc != nil && ipAlloc.ID != nil { + ipAllocID := *ipAlloc.ID + ipAllocName := azureshared.ExtractResourceName(ipAllocID) + if ipAllocName != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(ipAllocID); extractedScope != "" { + linkScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkIpAllocation.String(), + Method: sdp.QueryMethod_GET, + Query: ipAllocName, + Scope: linkScope, + }, + }) + } + } + } + } + + // Link to Network Interfaces that have IP configurations in this subnet (read-only references) + if subnet.Properties != nil && subnet.Properties.IPConfigurations != nil { + for _, ipConfig := range subnet.Properties.IPConfigurations { + if ipConfig != nil && ipConfig.ID != nil { + ipConfigID := *ipConfig.ID + // Format: .../networkInterfaces/{nicName}/ipConfigurations/{ipConfigName} + if strings.Contains(ipConfigID, "/networkInterfaces/") { + nicNames := azureshared.ExtractPathParamsFromResourceID(ipConfigID, []string{"networkInterfaces"}) + if len(nicNames) > 0 && nicNames[0] != "" { + linkScope := azureshared.ExtractScopeFromResourceID(ipConfigID) + if linkScope == "" { + linkScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkNetworkInterface.String(), + Method: sdp.QueryMethod_GET, + Query: nicNames[0], + Scope: linkScope, + }, + }) + } + } + } + } + } + + // Link to Application Gateways that have gateway IP configurations in this subnet (read-only references) + if subnet.Properties != nil && subnet.Properties.ApplicationGatewayIPConfigurations != nil { + for _, agIPConfig := range subnet.Properties.ApplicationGatewayIPConfigurations { + if agIPConfig != nil && agIPConfig.ID != nil { + agIPConfigID := *agIPConfig.ID + // Format: .../applicationGateways/{agName}/applicationGatewayIPConfigurations/... + agNames := azureshared.ExtractPathParamsFromResourceID(agIPConfigID, []string{"applicationGateways"}) + if len(agNames) > 0 && agNames[0] != "" { + linkScope := azureshared.ExtractScopeFromResourceID(agIPConfigID) + if linkScope == "" { + linkScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkApplicationGateway.String(), + Method: sdp.QueryMethod_GET, + Query: agNames[0], + Scope: linkScope, + }, + }) + } + } + } + } + + // Link to external resources referenced by ResourceNavigationLinks (e.g. SQL Managed Instance) + if subnet.Properties != nil && subnet.Properties.ResourceNavigationLinks != nil { + for _, rnl := range subnet.Properties.ResourceNavigationLinks { + if rnl != nil && rnl.Properties != nil && rnl.Properties.Link != nil { + linkID := *rnl.Properties.Link + resourceName := azureshared.ExtractResourceName(linkID) + if resourceName != "" { + linkScope := azureshared.ExtractScopeFromResourceID(linkID) + if linkScope == "" { + linkScope = scope + } + itemType := azureshared.ItemTypeFromLinkedResourceID(linkID) + if itemType == "" { + itemType = "azure-resource" + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: itemType, + Method: sdp.QueryMethod_GET, + Query: resourceName, + Scope: linkScope, + }, + }) + } + } + } + } + + // Link to external resources referenced by ServiceAssociationLinks (e.g. App Service Environment) + if subnet.Properties != nil && subnet.Properties.ServiceAssociationLinks != nil { + for _, sal := range subnet.Properties.ServiceAssociationLinks { + if sal != nil && sal.Properties != nil && sal.Properties.Link != nil { + linkID := *sal.Properties.Link + resourceName := azureshared.ExtractResourceName(linkID) + if resourceName != "" { + linkScope := azureshared.ExtractScopeFromResourceID(linkID) + if linkScope == "" { + linkScope = scope + } + itemType := azureshared.ItemTypeFromLinkedResourceID(linkID) + if itemType == "" { + itemType = "azure-resource" + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: itemType, + Method: sdp.QueryMethod_GET, + Query: resourceName, + Scope: linkScope, + }, + }) + } + } + } + } + + return sdpItem, nil +} + +func (n networkSubnetWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_subnet.id", + }, + } +} + +func (n networkSubnetWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Network/virtualNetworks/subnets/read", + } +} + +func (n networkSubnetWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/network-subnet_test.go b/sources/azure/manual/network-subnet_test.go new file mode 100644 index 00000000..0e2399cb --- /dev/null +++ b/sources/azure/manual/network-subnet_test.go @@ -0,0 +1,278 @@ +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/network/armnetwork/v8" + "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" +) + +type mockSubnetsPager struct { + pages []armnetwork.SubnetsClientListResponse + index int +} + +func (m *mockSubnetsPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockSubnetsPager) NextPage(ctx context.Context) (armnetwork.SubnetsClientListResponse, error) { + if m.index >= len(m.pages) { + return armnetwork.SubnetsClientListResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type errorSubnetsPager struct{} + +func (e *errorSubnetsPager) More() bool { + return true +} + +func (e *errorSubnetsPager) NextPage(ctx context.Context) (armnetwork.SubnetsClientListResponse, error) { + return armnetwork.SubnetsClientListResponse{}, errors.New("pager error") +} + +type testSubnetsClient struct { + *mocks.MockSubnetsClient + pager clients.SubnetsPager +} + +func (t *testSubnetsClient) NewListPager(resourceGroupName, virtualNetworkName string, options *armnetwork.SubnetsClientListOptions) clients.SubnetsPager { + return t.pager +} + +func TestNetworkSubnet(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + virtualNetworkName := "test-vnet" + subnetName := "test-subnet" + + t.Run("Get", func(t *testing.T) { + subnet := createAzureSubnet(subnetName, virtualNetworkName) + + mockClient := mocks.NewMockSubnetsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, virtualNetworkName, subnetName, nil).Return( + armnetwork.SubnetsClientGetResponse{ + Subnet: *subnet, + }, nil) + + testClient := &testSubnetsClient{MockSubnetsClient: mockClient} + wrapper := manual.NewNetworkSubnet(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(virtualNetworkName, subnetName) + 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.NetworkSubnet.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkSubnet, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != shared.CompositeLookupKey(virtualNetworkName, subnetName) { + t.Errorf("Expected unique attribute value %s, got %s", shared.CompositeLookupKey(virtualNetworkName, subnetName), sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.NetworkVirtualNetwork.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: virtualNetworkName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSubnetsClient(ctrl) + testClient := &testSubnetsClient{MockSubnetsClient: mockClient} + + wrapper := manual.NewNetworkSubnet(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], virtualNetworkName, true) + if qErr == nil { + t.Error("Expected error when providing insufficient query parts, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + subnet1 := createAzureSubnet("subnet-1", virtualNetworkName) + subnet2 := createAzureSubnet("subnet-2", virtualNetworkName) + + mockClient := mocks.NewMockSubnetsClient(ctrl) + mockPager := &mockSubnetsPager{ + pages: []armnetwork.SubnetsClientListResponse{ + { + SubnetListResult: armnetwork.SubnetListResult{ + Value: []*armnetwork.Subnet{subnet1, subnet2}, + }, + }, + }, + } + + testClient := &testSubnetsClient{ + MockSubnetsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewNetworkSubnet(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, wrapper.Scopes()[0], virtualNetworkName, 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.Fatalf("Expected no validation error, got: %v", err) + } + if item.GetType() != azureshared.NetworkSubnet.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkSubnet, item.GetType()) + } + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSubnetsClient(ctrl) + testClient := &testSubnetsClient{MockSubnetsClient: mockClient} + + wrapper := manual.NewNetworkSubnet(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("Search_SubnetWithNilName", func(t *testing.T) { + validSubnet := createAzureSubnet("valid-subnet", virtualNetworkName) + + mockClient := mocks.NewMockSubnetsClient(ctrl) + mockPager := &mockSubnetsPager{ + pages: []armnetwork.SubnetsClientListResponse{ + { + SubnetListResult: armnetwork.SubnetListResult{ + Value: []*armnetwork.Subnet{ + {Name: nil, ID: to.Ptr("/some/id")}, + validSubnet, + }, + }, + }, + }, + } + + testClient := &testSubnetsClient{ + MockSubnetsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewNetworkSubnet(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable := adapter.(discovery.SearchableAdapter) + sdpItems, err := searchable.Search(ctx, wrapper.Scopes()[0], virtualNetworkName, 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)) + } + if sdpItems[0].UniqueAttributeValue() != shared.CompositeLookupKey(virtualNetworkName, "valid-subnet") { + t.Errorf("Expected unique value %s, got %s", shared.CompositeLookupKey(virtualNetworkName, "valid-subnet"), sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("subnet not found") + + mockClient := mocks.NewMockSubnetsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, virtualNetworkName, "nonexistent-subnet", nil).Return( + armnetwork.SubnetsClientGetResponse{}, expectedErr) + + testClient := &testSubnetsClient{MockSubnetsClient: mockClient} + wrapper := manual.NewNetworkSubnet(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(virtualNetworkName, "nonexistent-subnet") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent subnet, but got nil") + } + }) + + t.Run("ErrorHandling_Search", func(t *testing.T) { + mockClient := mocks.NewMockSubnetsClient(ctrl) + testClient := &testSubnetsClient{ + MockSubnetsClient: mockClient, + pager: &errorSubnetsPager{}, + } + + wrapper := manual.NewNetworkSubnet(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable := adapter.(discovery.SearchableAdapter) + _, err := searchable.Search(ctx, wrapper.Scopes()[0], virtualNetworkName, true) + if err == nil { + t.Error("Expected error from pager when NextPage returns an error, but got nil") + } + }) +} + +func createAzureSubnet(subnetName, vnetName string) *armnetwork.Subnet { + return &armnetwork.Subnet{ + ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/" + vnetName + "/subnets/" + subnetName), + Name: to.Ptr(subnetName), + Type: to.Ptr("Microsoft.Network/virtualNetworks/subnets"), + Properties: &armnetwork.SubnetPropertiesFormat{ + AddressPrefix: to.Ptr("10.0.0.0/24"), + }, + } +} diff --git a/sources/azure/shared/item-types.go b/sources/azure/shared/item-types.go index 41b2f74d..4be4f1ff 100644 --- a/sources/azure/shared/item-types.go +++ b/sources/azure/shared/item-types.go @@ -82,6 +82,8 @@ var ( NetworkDscpConfiguration = shared.NewItemType(Azure, Network, DscpConfiguration) NetworkVirtualNetworkTap = shared.NewItemType(Azure, Network, VirtualNetworkTap) NetworkNetworkInterfaceTapConfiguration = shared.NewItemType(Azure, Network, NetworkInterfaceTapConfiguration) + NetworkServiceEndpointPolicy = shared.NewItemType(Azure, Network, ServiceEndpointPolicy) + NetworkIpAllocation = shared.NewItemType(Azure, Network, IpAllocation) //Storage item types StorageAccount = shared.NewItemType(Azure, Storage, Account) diff --git a/sources/azure/shared/mocks/mock_subnets_client.go b/sources/azure/shared/mocks/mock_subnets_client.go new file mode 100644 index 00000000..0d6532e3 --- /dev/null +++ b/sources/azure/shared/mocks/mock_subnets_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: subnets-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_subnets_client.go -package=mocks -source=subnets-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockSubnetsClient is a mock of SubnetsClient interface. +type MockSubnetsClient struct { + ctrl *gomock.Controller + recorder *MockSubnetsClientMockRecorder + isgomock struct{} +} + +// MockSubnetsClientMockRecorder is the mock recorder for MockSubnetsClient. +type MockSubnetsClientMockRecorder struct { + mock *MockSubnetsClient +} + +// NewMockSubnetsClient creates a new mock instance. +func NewMockSubnetsClient(ctrl *gomock.Controller) *MockSubnetsClient { + mock := &MockSubnetsClient{ctrl: ctrl} + mock.recorder = &MockSubnetsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSubnetsClient) EXPECT() *MockSubnetsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockSubnetsClient) Get(ctx context.Context, resourceGroupName, virtualNetworkName, subnetName string, options *armnetwork.SubnetsClientGetOptions) (armnetwork.SubnetsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, virtualNetworkName, subnetName, options) + ret0, _ := ret[0].(armnetwork.SubnetsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockSubnetsClientMockRecorder) Get(ctx, resourceGroupName, virtualNetworkName, subnetName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSubnetsClient)(nil).Get), ctx, resourceGroupName, virtualNetworkName, subnetName, options) +} + +// NewListPager mocks base method. +func (m *MockSubnetsClient) NewListPager(resourceGroupName, virtualNetworkName string, options *armnetwork.SubnetsClientListOptions) clients.SubnetsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListPager", resourceGroupName, virtualNetworkName, options) + ret0, _ := ret[0].(clients.SubnetsPager) + return ret0 +} + +// NewListPager indicates an expected call of NewListPager. +func (mr *MockSubnetsClientMockRecorder) NewListPager(resourceGroupName, virtualNetworkName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListPager", reflect.TypeOf((*MockSubnetsClient)(nil).NewListPager), resourceGroupName, virtualNetworkName, options) +} diff --git a/sources/azure/shared/models.go b/sources/azure/shared/models.go index 0025c3c7..99785ce1 100644 --- a/sources/azure/shared/models.go +++ b/sources/azure/shared/models.go @@ -130,6 +130,8 @@ const ( DscpConfiguration shared.Resource = "dscp-configuration" VirtualNetworkTap shared.Resource = "virtual-network-tap" NetworkInterfaceTapConfiguration shared.Resource = "network-interface-tap-configuration" + ServiceEndpointPolicy shared.Resource = "service-endpoint-policy" + IpAllocation shared.Resource = "ip-allocation" // Storage resources Account shared.Resource = "account" diff --git a/sources/azure/shared/resource_id_item_type.go b/sources/azure/shared/resource_id_item_type.go new file mode 100644 index 00000000..58c9b249 --- /dev/null +++ b/sources/azure/shared/resource_id_item_type.go @@ -0,0 +1,98 @@ +package shared + +import ( + "strings" + "unicode" +) + +// azureProviderToAPI maps Azure resource provider namespaces to the short API names used in +// item types (see models.go). Enables generated linked queries to match existing adapter +// naming: azure-{api}-{resource} with kebab-case resource. +var azureProviderToAPI = map[string]string{ + "microsoft.compute": "compute", + "microsoft.network": "network", + "microsoft.storage": "storage", + "microsoft.sql": "sql", + "microsoft.documentdb": "documentdb", + "microsoft.keyvault": "keyvault", + "microsoft.managedidentity": "managedidentity", + "microsoft.batch": "batch", + "microsoft.dbforpostgresql": "dbforpostgresql", + "microsoft.elasticsan": "elasticsan", + "microsoft.authorization": "authorization", + "microsoft.maintenance": "maintenance", + "microsoft.resources": "resources", +} + +// CamelCaseToKebab converts Azure camelCase resource type (e.g. virtualNetworks, publicIPAddresses) +// to kebab-case (e.g. virtual-networks, public-ip-addresses) to match project convention in models.go. +// Consecutive uppercase letters are treated as a single acronym (e.g. IP stays together). +func CamelCaseToKebab(s string) string { + if s == "" { + return "" + } + var b strings.Builder + runes := []rune(s) + for i, r := range runes { + if unicode.IsUpper(r) { + prevLower := i > 0 && unicode.IsLower(runes[i-1]) + nextLower := i+1 < len(runes) && unicode.IsLower(runes[i+1]) + // Insert hyphen before uppercase when: after a lowercase letter, or when this uppercase starts a word (next is lower) + if i > 0 && (prevLower || (unicode.IsUpper(runes[i-1]) && nextLower)) { + b.WriteByte('-') + } + b.WriteRune(unicode.ToLower(r)) + } else { + b.WriteRune(unicode.ToLower(r)) + } + } + return b.String() +} + +// SingularizeResourceType converts Azure plural resource type to singular form to match +// models.go (e.g. virtual-networks -> virtual-network, galleries -> gallery, identities -> identity). +func SingularizeResourceType(kebab string) string { + if kebab == "" { + return kebab + } + // -ies -> -y (e.g. galleries -> gallery, user-assigned-identities -> user-assigned-identity) + if strings.HasSuffix(kebab, "ies") { + return strings.TrimSuffix(kebab, "ies") + "y" + } + // -addresses -> -address (e.g. public-ip-addresses -> public-ip-address) + if strings.HasSuffix(kebab, "addresses") { + return strings.TrimSuffix(kebab, "addresses") + "address" + } + if strings.HasSuffix(kebab, "s") { + return strings.TrimSuffix(kebab, "s") + } + return kebab +} + +// ItemTypeFromLinkedResourceID derives an item type string from an Azure resource ID for use in +// LinkedItemQueries (e.g. ResourceNavigationLink, ServiceAssociationLink). Uses short API names +// and kebab-case singular resource types so generated types match existing adapter naming +// (e.g. azure-network-virtual-network). For unknown providers, returns empty so callers can +// fall back to a generic type such as "azure-resource". +func ItemTypeFromLinkedResourceID(resourceID string) string { + if resourceID == "" { + return "" + } + parts := strings.Split(strings.Trim(resourceID, "/"), "/") + for i, part := range parts { + if strings.EqualFold(part, "providers") && i+2 < len(parts) { + provider := strings.ToLower(parts[i+1]) + resourceTypeRaw := parts[i+2] + api, ok := azureProviderToAPI[provider] + if !ok { + return "" + } + resourceType := SingularizeResourceType(CamelCaseToKebab(resourceTypeRaw)) + if resourceType == "" { + return "" + } + return "azure-" + api + "-" + resourceType + } + } + return "" +} diff --git a/sources/azure/shared/resource_id_item_type_test.go b/sources/azure/shared/resource_id_item_type_test.go new file mode 100644 index 00000000..ae86fba5 --- /dev/null +++ b/sources/azure/shared/resource_id_item_type_test.go @@ -0,0 +1,116 @@ +package shared_test + +import ( + "testing" + + azureshared "github.com/overmindtech/cli/sources/azure/shared" +) + +func TestCamelCaseToKebab(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"virtualNetworks", "virtualNetworks", "virtual-networks"}, + {"managedInstances", "managedInstances", "managed-instances"}, + {"applicationGateways", "applicationGateways", "application-gateways"}, + {"publicIPAddresses (acronym)", "publicIPAddresses", "public-ip-addresses"}, + {"empty", "", ""}, + {"single word lowercase", "subnet", "subnet"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := azureshared.CamelCaseToKebab(tc.input) + if got != tc.expected { + t.Errorf("CamelCaseToKebab(%q) = %q; want %q", tc.input, got, tc.expected) + } + }) + } +} + +func TestSingularizeResourceType(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"virtual-networks", "virtual-networks", "virtual-network"}, + {"managed-instances", "managed-instances", "managed-instance"}, + {"galleries -> gallery", "galleries", "gallery"}, + {"user-assigned-identities -> user-assigned-identity", "user-assigned-identities", "user-assigned-identity"}, + {"public-ip-addresses -> public-ip-address", "public-ip-addresses", "public-ip-address"}, + {"no trailing s", "virtual-network", "virtual-network"}, + {"empty", "", ""}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := azureshared.SingularizeResourceType(tc.input) + if got != tc.expected { + t.Errorf("SingularizeResourceType(%q) = %q; want %q", tc.input, got, tc.expected) + } + }) + } +} + +func TestItemTypeFromLinkedResourceID(t *testing.T) { + tests := []struct { + name string + resourceID string + expected string + }{ + { + name: "Microsoft.Network virtualNetworks", + resourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Network/virtualNetworks/myVnet", + expected: "azure-network-virtual-network", + }, + { + name: "Microsoft.Sql managedInstances", + resourceID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myRg/providers/Microsoft.Sql/managedInstances/myMI", + expected: "azure-sql-managed-instance", + }, + { + name: "Microsoft.Compute virtualMachines", + resourceID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/vm1", + expected: "azure-compute-virtual-machine", + }, + { + name: "unknown provider returns empty", + resourceID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Unknown/fooBars/name", + expected: "", + }, + { + name: "empty ID returns empty", + resourceID: "", + expected: "", + }, + { + name: "no providers segment returns empty", + resourceID: "/not/a/valid/resource/id", + expected: "", + }, + { + name: "Microsoft.Compute galleries", + resourceID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/galleries/myGallery", + expected: "azure-compute-gallery", + }, + { + name: "Microsoft.ManagedIdentity userAssignedIdentities", + resourceID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myIdentity", + expected: "azure-managedidentity-user-assigned-identity", + }, + { + name: "Microsoft.Network publicIPAddresses (acronym)", + resourceID: "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/myPublicIP", + expected: "azure-network-public-ip-address", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := azureshared.ItemTypeFromLinkedResourceID(tc.resourceID) + if got != tc.expected { + t.Errorf("ItemTypeFromLinkedResourceID(%q) = %q; want %q", tc.resourceID, got, tc.expected) + } + }) + } +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 941ea07f..63557c14 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -30,6 +30,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "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}", + "azure-network-subnet": {"virtualNetworks", "subnets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName}", } if keys, ok := pathKeysMap[resourceType]; ok { From 936dec41d164e4c1ae7c892bcc906fef4917e829 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:23:29 +0000 Subject: [PATCH 05/74] Add Azure Gallery Applications Client and Adapter (#3986) image > [!NOTE] > **Medium Risk** > Adds a new Azure discovery adapter and wires it into initialization, which may increase API calls and affect discovery/linking behavior, but changes are additive and covered by unit tests. > > **Overview** > Adds first-class support for Azure Compute Shared Image Gallery *applications* by introducing a `GalleryApplicationsClient` wrapper and registering it in `manual/adapters.go` for both real and placeholder adapter initialization. > > Introduces a new `ComputeGalleryApplication` adapter with `Get`/`Search`/streaming search, unique keying by `galleryName+applicationName`, and linked queries to the parent gallery, child application versions, and URI-derived network resources; includes unit tests plus generated GoMock clients, and updates Azure resource-ID path key parsing to recognize `azure-compute-gallery-application`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit eb5ec855013b3a2783fd868617d2d1fd036d9376. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 50ba559650c678bcf33d3d05104328c82bc999d3 --- .../clients/gallery-applications-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + .../compute-gallery-application-version.go | 1 - .../manual/compute-gallery-application.go | 250 ++++++++++++++ .../compute-gallery-application_test.go | 305 ++++++++++++++++++ .../mock_gallery_applications_client_test.go | 72 +++++ .../mocks/mock_gallery_applications_client.go | 72 +++++ sources/azure/shared/utils.go | 1 + 8 files changed, 745 insertions(+), 1 deletion(-) create mode 100644 sources/azure/clients/gallery-applications-client.go create mode 100644 sources/azure/manual/compute-gallery-application.go create mode 100644 sources/azure/manual/compute-gallery-application_test.go create mode 100644 sources/azure/manual/mock_gallery_applications_client_test.go create mode 100644 sources/azure/shared/mocks/mock_gallery_applications_client.go diff --git a/sources/azure/clients/gallery-applications-client.go b/sources/azure/clients/gallery-applications-client.go new file mode 100644 index 00000000..b7194f54 --- /dev/null +++ b/sources/azure/clients/gallery-applications-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_gallery_applications_client.go -package=mocks -source=gallery-applications-client.go + +// GalleryApplicationsPager is a type alias for the generic Pager interface with gallery application response type. +type GalleryApplicationsPager = Pager[armcompute.GalleryApplicationsClientListByGalleryResponse] + +// GalleryApplicationsClient is an interface for interacting with Azure gallery applications +type GalleryApplicationsClient interface { + NewListByGalleryPager(resourceGroupName string, galleryName string, options *armcompute.GalleryApplicationsClientListByGalleryOptions) GalleryApplicationsPager + Get(ctx context.Context, resourceGroupName string, galleryName string, galleryApplicationName string, options *armcompute.GalleryApplicationsClientGetOptions) (armcompute.GalleryApplicationsClientGetResponse, error) +} + +type galleryApplicationsClient struct { + client *armcompute.GalleryApplicationsClient +} + +func (c *galleryApplicationsClient) NewListByGalleryPager(resourceGroupName string, galleryName string, options *armcompute.GalleryApplicationsClientListByGalleryOptions) GalleryApplicationsPager { + return c.client.NewListByGalleryPager(resourceGroupName, galleryName, options) +} + +func (c *galleryApplicationsClient) Get(ctx context.Context, resourceGroupName string, galleryName string, galleryApplicationName string, options *armcompute.GalleryApplicationsClientGetOptions) (armcompute.GalleryApplicationsClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, galleryName, galleryApplicationName, options) +} + +// NewGalleryApplicationsClient creates a new GalleryApplicationsClient from the Azure SDK client +func NewGalleryApplicationsClient(client *armcompute.GalleryApplicationsClient) GalleryApplicationsClient { + return &galleryApplicationsClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index bdf0799f..88b9cc6a 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 application versions client: %w", err) } + galleryApplicationsClient, err := armcompute.NewGalleryApplicationsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create gallery applications 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) @@ -438,6 +443,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewComputeGalleryApplication( + clients.NewGalleryApplicationsClient(galleryApplicationsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewComputeGallery( clients.NewGalleriesClient(galleriesClient), resourceGroupScopes, @@ -511,6 +520,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(NewComputeGalleryApplication(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeGallery(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeGalleryImage(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeSnapshot(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/compute-gallery-application-version.go b/sources/azure/manual/compute-gallery-application-version.go index abee7c96..dc863f58 100644 --- a/sources/azure/manual/compute-gallery-application-version.go +++ b/sources/azure/manual/compute-gallery-application-version.go @@ -18,7 +18,6 @@ import ( var ( ComputeGalleryApplicationVersionLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGalleryApplicationVersion) - ComputeGalleryApplicationLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGalleryApplication) //todo: move to its adapter file when created, this is just a placeholder ) type computeGalleryApplicationVersionWrapper struct { diff --git a/sources/azure/manual/compute-gallery-application.go b/sources/azure/manual/compute-gallery-application.go new file mode 100644 index 00000000..5fa2e23d --- /dev/null +++ b/sources/azure/manual/compute-gallery-application.go @@ -0,0 +1,250 @@ +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 ( + ComputeGalleryApplicationLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeGalleryApplication) +) + +type computeGalleryApplicationWrapper struct { + client clients.GalleryApplicationsClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeGalleryApplication(client clients.GalleryApplicationsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &computeGalleryApplicationWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeGalleryApplication, + ), + } +} + +func (c computeGalleryApplicationWrapper) 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 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()) + } + resp, err := c.client.Get(ctx, rgScope.ResourceGroup, galleryName, galleryApplicationName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureGalleryApplicationToSDPItem(&resp.GalleryApplication, galleryName, scope) +} + +func (c computeGalleryApplicationWrapper) 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 _, galleryApplication := range page.Value { + if galleryApplication == nil || galleryApplication.Name == nil { + continue + } + item, sdpErr := c.azureGalleryApplicationToSDPItem(galleryApplication, galleryName, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c computeGalleryApplicationWrapper) 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 _, galleryApplication := range page.Value { + if galleryApplication == nil || galleryApplication.Name == nil { + continue + } + item, sdpErr := c.azureGalleryApplicationToSDPItem(galleryApplication, galleryName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (c computeGalleryApplicationWrapper) azureGalleryApplicationToSDPItem( + galleryApplication *armcompute.GalleryApplication, + galleryName, + scope string, +) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(galleryApplication, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + if galleryApplication.Name == nil { + return nil, azureshared.QueryError(errors.New("gallery application name is nil"), scope, c.Type()) + } + galleryApplicationName := *galleryApplication.Name + if galleryApplicationName == "" { + return nil, azureshared.QueryError(errors.New("gallery application name cannot be empty"), scope, c.Type()) + } + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(galleryName, galleryApplicationName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + + // Parent Gallery: application depends on gallery + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeGallery.String(), + Method: sdp.QueryMethod_GET, + Query: galleryName, + Scope: scope, + }, + }) + + // Child: list gallery application versions under this application (Search by gallery name + application name) + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeGalleryApplicationVersion.String(), + Method: sdp.QueryMethod_SEARCH, + Query: shared.CompositeLookupKey(galleryName, galleryApplicationName), + Scope: scope, + }, + }) + + // URI-based links: Eula, PrivacyStatementURI, ReleaseNoteURI + linkedDNSHostnames := make(map[string]struct{}) + seenIPs := make(map[string]struct{}) + if galleryApplication.Properties != nil { + if galleryApplication.Properties.Eula != nil && *galleryApplication.Properties.Eula != "" { + AppendURILinks(&linkedItemQueries, *galleryApplication.Properties.Eula, linkedDNSHostnames, seenIPs) + } + if galleryApplication.Properties.PrivacyStatementURI != nil && *galleryApplication.Properties.PrivacyStatementURI != "" { + AppendURILinks(&linkedItemQueries, *galleryApplication.Properties.PrivacyStatementURI, linkedDNSHostnames, seenIPs) + } + if galleryApplication.Properties.ReleaseNoteURI != nil && *galleryApplication.Properties.ReleaseNoteURI != "" { + AppendURILinks(&linkedItemQueries, *galleryApplication.Properties.ReleaseNoteURI, linkedDNSHostnames, seenIPs) + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeGalleryApplication.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(galleryApplication.Tags), + LinkedItemQueries: linkedItemQueries, + } + return sdpItem, nil +} + +func (c computeGalleryApplicationWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeGalleryLookupByName, + ComputeGalleryApplicationLookupByName, + } +} + +func (c computeGalleryApplicationWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + ComputeGalleryLookupByName, + }, + } +} + +func (c computeGalleryApplicationWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.ComputeGallery, + azureshared.ComputeGalleryApplicationVersion, + stdlib.NetworkDNS, + stdlib.NetworkHTTP, + stdlib.NetworkIP, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/gallery_application +func (c computeGalleryApplicationWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_gallery_application.id", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/compute#microsoftcompute +func (c computeGalleryApplicationWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/galleries/applications/read", + } +} + +func (c computeGalleryApplicationWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-gallery-application_test.go b/sources/azure/manual/compute-gallery-application_test.go new file mode 100644 index 00000000..c1ce85a4 --- /dev/null +++ b/sources/azure/manual/compute-gallery-application_test.go @@ -0,0 +1,305 @@ +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" +) + +// mockGalleryApplicationsPager is a mock pager for ListByGallery. +type mockGalleryApplicationsPager struct { + pages []armcompute.GalleryApplicationsClientListByGalleryResponse + index int +} + +func (m *mockGalleryApplicationsPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockGalleryApplicationsPager) NextPage(ctx context.Context) (armcompute.GalleryApplicationsClientListByGalleryResponse, error) { + if m.index >= len(m.pages) { + return armcompute.GalleryApplicationsClientListByGalleryResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +// errorGalleryApplicationsPager is a mock pager that always returns an error. +type errorGalleryApplicationsPager struct{} + +func (e *errorGalleryApplicationsPager) More() bool { + return true +} + +func (e *errorGalleryApplicationsPager) NextPage(ctx context.Context) (armcompute.GalleryApplicationsClientListByGalleryResponse, error) { + return armcompute.GalleryApplicationsClientListByGalleryResponse{}, errors.New("pager error") +} + +// testGalleryApplicationsClient wraps the mock and returns a pager from NewListByGalleryPager. +type testGalleryApplicationsClient struct { + *MockGalleryApplicationsClient + pager clients.GalleryApplicationsPager +} + +func (t *testGalleryApplicationsClient) NewListByGalleryPager(resourceGroupName, galleryName string, options *armcompute.GalleryApplicationsClientListByGalleryOptions) clients.GalleryApplicationsPager { + if t.pager != nil { + return t.pager + } + return t.MockGalleryApplicationsClient.NewListByGalleryPager(resourceGroupName, galleryName, options) +} + +func createAzureGalleryApplication(applicationName string) *armcompute.GalleryApplication { + return &armcompute.GalleryApplication{ + Name: to.Ptr(applicationName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + }, + Properties: &armcompute.GalleryApplicationProperties{ + SupportedOSType: to.Ptr(armcompute.OperatingSystemTypesWindows), + Description: to.Ptr("Test gallery application"), + }, + } +} + +func TestComputeGalleryApplication(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-application" + + t.Run("Get", func(t *testing.T) { + app := createAzureGalleryApplication(galleryApplicationName) + + mockClient := NewMockGalleryApplicationsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, nil).Return( + armcompute.GalleryApplicationsClientGetResponse{ + GalleryApplication: *app, + }, nil) + + wrapper := NewComputeGalleryApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(galleryName, galleryApplicationName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeGalleryApplication.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeGalleryApplication.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUnique := shared.CompositeLookupKey(galleryName, galleryApplicationName) + if sdpItem.UniqueAttributeValue() != expectedUnique { + t.Errorf("Expected unique attribute value %s, got %s", expectedUnique, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetTags()["env"] != "test" { + t.Errorf("Expected tag env=test, got: %v", sdpItem.GetTags()["env"]) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeGallery.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: galleryName, ExpectedScope: scope}, + {ExpectedType: azureshared.ComputeGalleryApplicationVersion.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: shared.CompositeLookupKey(galleryName, galleryApplicationName), ExpectedScope: scope}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := NewMockGalleryApplicationsClient(ctrl) + wrapper := NewComputeGalleryApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, 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 := NewMockGalleryApplicationsClient(ctrl) + wrapper := NewComputeGalleryApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey("", galleryApplicationName) + _, 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_EmptyApplicationName", func(t *testing.T) { + mockClient := NewMockGalleryApplicationsClient(ctrl) + wrapper := NewComputeGalleryApplication(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 gallery application name is empty, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("application not found") + mockClient := NewMockGalleryApplicationsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, "nonexistent", nil).Return( + armcompute.GalleryApplicationsClientGetResponse{}, expectedErr) + + wrapper := NewComputeGalleryApplication(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) { + app1 := createAzureGalleryApplication("app-1") + app2 := createAzureGalleryApplication("app-2") + + mockClient := NewMockGalleryApplicationsClient(ctrl) + pages := []armcompute.GalleryApplicationsClientListByGalleryResponse{ + { + GalleryApplicationList: armcompute.GalleryApplicationList{ + Value: []*armcompute.GalleryApplication{app1, app2}, + }, + }, + } + mockPager := &mockGalleryApplicationsPager{pages: pages} + testClient := &testGalleryApplicationsClient{ + MockGalleryApplicationsClient: mockClient, + pager: mockPager, + } + + wrapper := NewComputeGalleryApplication(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 := NewMockGalleryApplicationsClient(ctrl) + wrapper := NewComputeGalleryApplication(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") + } + + searchQuery := shared.CompositeLookupKey(galleryName, galleryApplicationName) + _, 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 := NewMockGalleryApplicationsClient(ctrl) + wrapper := NewComputeGalleryApplication(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 := NewMockGalleryApplicationsClient(ctrl) + errorPager := &errorGalleryApplicationsPager{} + testClient := &testGalleryApplicationsClient{ + MockGalleryApplicationsClient: mockClient, + pager: errorPager, + } + + wrapper := NewComputeGalleryApplication(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 := NewMockGalleryApplicationsClient(ctrl) + wrapper := NewComputeGalleryApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + links := wrapper.PotentialLinks() + expected := map[shared.ItemType]bool{ + azureshared.ComputeGallery: true, + azureshared.ComputeGalleryApplicationVersion: 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 := NewMockGalleryApplicationsClient(ctrl) + wrapper := NewComputeGalleryApplication(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/mock_gallery_applications_client_test.go b/sources/azure/manual/mock_gallery_applications_client_test.go new file mode 100644 index 00000000..e317c794 --- /dev/null +++ b/sources/azure/manual/mock_gallery_applications_client_test.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: sources/azure/clients/gallery-applications-client.go +// +// Generated by this command: +// +// mockgen -destination=sources/azure/manual/mock_gallery_applications_client_test.go -package=manual -source=sources/azure/clients/gallery-applications-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" +) + +// MockGalleryApplicationsClient is a mock of GalleryApplicationsClient interface. +type MockGalleryApplicationsClient struct { + ctrl *gomock.Controller + recorder *MockGalleryApplicationsClientMockRecorder + isgomock struct{} +} + +// MockGalleryApplicationsClientMockRecorder is the mock recorder for MockGalleryApplicationsClient. +type MockGalleryApplicationsClientMockRecorder struct { + mock *MockGalleryApplicationsClient +} + +// NewMockGalleryApplicationsClient creates a new mock instance. +func NewMockGalleryApplicationsClient(ctrl *gomock.Controller) *MockGalleryApplicationsClient { + mock := &MockGalleryApplicationsClient{ctrl: ctrl} + mock.recorder = &MockGalleryApplicationsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGalleryApplicationsClient) EXPECT() *MockGalleryApplicationsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockGalleryApplicationsClient) Get(ctx context.Context, resourceGroupName, galleryName, galleryApplicationName string, options *armcompute.GalleryApplicationsClientGetOptions) (armcompute.GalleryApplicationsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, galleryApplicationName, options) + ret0, _ := ret[0].(armcompute.GalleryApplicationsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockGalleryApplicationsClientMockRecorder) Get(ctx, resourceGroupName, galleryName, galleryApplicationName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleryApplicationsClient)(nil).Get), ctx, resourceGroupName, galleryName, galleryApplicationName, options) +} + +// NewListByGalleryPager mocks base method. +func (m *MockGalleryApplicationsClient) NewListByGalleryPager(resourceGroupName, galleryName string, options *armcompute.GalleryApplicationsClientListByGalleryOptions) clients.GalleryApplicationsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByGalleryPager", resourceGroupName, galleryName, options) + ret0, _ := ret[0].(clients.GalleryApplicationsPager) + return ret0 +} + +// NewListByGalleryPager indicates an expected call of NewListByGalleryPager. +func (mr *MockGalleryApplicationsClientMockRecorder) NewListByGalleryPager(resourceGroupName, galleryName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByGalleryPager", reflect.TypeOf((*MockGalleryApplicationsClient)(nil).NewListByGalleryPager), resourceGroupName, galleryName, options) +} diff --git a/sources/azure/shared/mocks/mock_gallery_applications_client.go b/sources/azure/shared/mocks/mock_gallery_applications_client.go new file mode 100644 index 00000000..9bd7fd45 --- /dev/null +++ b/sources/azure/shared/mocks/mock_gallery_applications_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: gallery-applications-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_gallery_applications_client.go -package=mocks -source=gallery-applications-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" +) + +// MockGalleryApplicationsClient is a mock of GalleryApplicationsClient interface. +type MockGalleryApplicationsClient struct { + ctrl *gomock.Controller + recorder *MockGalleryApplicationsClientMockRecorder + isgomock struct{} +} + +// MockGalleryApplicationsClientMockRecorder is the mock recorder for MockGalleryApplicationsClient. +type MockGalleryApplicationsClientMockRecorder struct { + mock *MockGalleryApplicationsClient +} + +// NewMockGalleryApplicationsClient creates a new mock instance. +func NewMockGalleryApplicationsClient(ctrl *gomock.Controller) *MockGalleryApplicationsClient { + mock := &MockGalleryApplicationsClient{ctrl: ctrl} + mock.recorder = &MockGalleryApplicationsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGalleryApplicationsClient) EXPECT() *MockGalleryApplicationsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockGalleryApplicationsClient) Get(ctx context.Context, resourceGroupName, galleryName, galleryApplicationName string, options *armcompute.GalleryApplicationsClientGetOptions) (armcompute.GalleryApplicationsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, galleryApplicationName, options) + ret0, _ := ret[0].(armcompute.GalleryApplicationsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockGalleryApplicationsClientMockRecorder) Get(ctx, resourceGroupName, galleryName, galleryApplicationName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleryApplicationsClient)(nil).Get), ctx, resourceGroupName, galleryName, galleryApplicationName, options) +} + +// NewListByGalleryPager mocks base method. +func (m *MockGalleryApplicationsClient) NewListByGalleryPager(resourceGroupName, galleryName string, options *armcompute.GalleryApplicationsClientListByGalleryOptions) clients.GalleryApplicationsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByGalleryPager", resourceGroupName, galleryName, options) + ret0, _ := ret[0].(clients.GalleryApplicationsPager) + return ret0 +} + +// NewListByGalleryPager indicates an expected call of NewListByGalleryPager. +func (mr *MockGalleryApplicationsClientMockRecorder) NewListByGalleryPager(resourceGroupName, galleryName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByGalleryPager", reflect.TypeOf((*MockGalleryApplicationsClient)(nil).NewListByGalleryPager), resourceGroupName, galleryName, options) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 63557c14..3c08eeda 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-application": {"galleries", "applications"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{applicationName}", "azure-compute-gallery-image": {"galleries", "images"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/images/{imageName}", "azure-network-subnet": {"virtualNetworks", "subnets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName}", } From 8ed02dbf3cb62dfc72ee98454726710fe90dd1eb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:29:41 +0000 Subject: [PATCH 06/74] chore(deps): lock file maintenance (#3969) 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). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> GitOrigin-RevId: 274f0481f6976247f3788133c2741c8b70edd9a2 --- .terraform.lock.hcl | 58 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index 589f730a..c59993e2 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.1" + version = "6.33.0" constraints = ">= 4.56.0" hashes = [ - "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", + "h1:5WOFa3rTDasweoSDN+YUVI4X0sjP8Z/gPCp1yWBzESw=", + "h1:DAUdRFTP+CSl97kUK1ixUEfo4gzFeqDSpqas82B+/JA=", + "h1:MxndtTQD9qHGQwIjScLlY4BQZgdjJ1Lsq1TdwodsxmU=", + "h1:OHXZ+2JAjhcjQAItUsd1beQkdNoTTwvQN9YZG894IsU=", + "h1:c54PuIr7mP2SYdPG2yayENMUGgSJlOmHKOFZAXKjYiM=", + "h1:eHK7wxG22vsMlKQwaPCUCGCu6H7rFF7TgjHY05M+8DE=", + "h1:eho379mi4YF9Lue1Xpq+vpvZzV+kqP9l4QssnDNJgkA=", + "h1:gWcYhi/dga/1U5HRxc2p9fgODsenO7qQ2lXaRwOHfIc=", + "h1:h+u+IPB/QmydJZHUepbvkCWSR2LSZ9rl6xXURbQdbXQ=", + "h1:hrMUZRM4IVU61ZwpLW6CMVmhlQSOajv8L5hSZmVNvQY=", + "h1:iy/Wyzeat5RuwUwENTfWHeXG/sXnTianugicM64kYj0=", + "h1:vwOi4EvCXxCaYLtCy2vaNkKGHNPvz1Oz2ySn3eRtQ50=", + "h1:wNrviem6bg9fq1bYvtGqH9QWO6iWbM1bBRLSqFJWqWM=", + "h1:zBnDfDt2pekMN9B43NJdiDerHvZENG6Wh6okZWusuOs=", + "zh:207f3f9db05c11429a241b84deeecfbd4caa941792c2c49b09c8c85cd59474dd", + "zh:25c36ad1f4617aeb23f8cd18efc7856127db721f6cf3e2e474236af019ce9ad1", + "zh:2685af1f3eb9abfce3168777463eaaad9dba5687f9f84d8bb579cb878bcfa18b", + "zh:57e28457952cf43923533af0a9bb322164be5fc3d66c080b5c59ee81950e9ef6", + "zh:5b6cd074f9e3a8d91841e739d259fe11f181e69c4019e3321231b35c0dde08c8", + "zh:6e3251500cebf1effb9c68d49041268ea270f75b122b94d261af231a8ebfa981", + "zh:7eee56f52f4b94637793508f3e83f68855f5f884a77aed2bd2fe77480c89e33d", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:9e3d4d1848fc6675c6bd88087188f229c4ec98b1a35de97c2697a0160fb76678", - "zh:b21c1b932c896c21988baac3b1cbc8b51843581b8fabf5e396952a329c9e6a12", - "zh:df8e5b1a2713880e2b3c489cc22ad3b14490e1702a1637273f91747bf091c071", - "zh:ec66785d40f7c04f138bb94fec55b8ddaae6fcc9cb25cc388989150bfaf2de4c", - "zh:f1ecb00fcfdb0c2aec3622549c023f469db401f395bc25bbddfe5cf8b51cd046", - "zh:fca78bf28897c8077130ce8d0f4d67900dbd77619adb1326bcd017ef421e5f1f", + "zh:9e228c92db1b9e36a0f899d6ab7446e6b8cf3183112d4f1c1613d6827a0ed3d6", + "zh:b34a84475e91715352ed1119b21e51a81d8ad12e93c86d4e78cd2d315d02dcab", + "zh:cdcc05a423a78a9b2c4e2844c58ecbf2ce6a3117cab353fa05197782d6f76667", + "zh:d0f5f6b1399cfa1b64f3e824bee9e39ff15d5a540ff197e9bfc157fe354a8426", + "zh:d9525dbb53468dee6b8e6d15669d25957e9872bf1cd386231dff93c8c659f1d7", + "zh:ed37db2df08b961a7fc390164273e602767ca6922f57560daa9678a2e1315fd0", + "zh:f6adc66b86e12041a2d3739600e6a153a1f5752dd363db11469f6f4dbd090080", ] } From 0ba563f2d1206e766766a6eb5df85a87a0c02429 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:30:37 +0000 Subject: [PATCH 07/74] fix(deps): update google.golang.org/genproto/googleapis/rpc digest to 2f722ef (#3951) 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 | `4cfbd41` → `2f722ef` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/370) 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: 3c95d4397b7dbd6b25f57035eae721715f784ffe --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 95ab4cf0..41a612a1 100644 --- a/go.mod +++ b/go.mod @@ -187,7 +187,7 @@ require ( gonum.org/v1/gonum v0.17.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/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 gopkg.in/ini.v1 v1.67.1 diff --git a/go.sum b/go.sum index 1b0cecf6..9dbf56cc 100644 --- a/go.sum +++ b/go.sum @@ -1488,8 +1488,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-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/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc h1:51Wupg8spF+5FC6D+iMKbOddFjMckETnNnEiZ+HX37s= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= 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 34813856c2a419d133f5595406ab7548e7003700 Mon Sep 17 00:00:00 2001 From: Elliot Waddington Date: Tue, 24 Feb 2026 18:40:02 +0100 Subject: [PATCH 08/74] Final blast propagation cleanup (#3985) Removes outdated references to hardcoded blast propagation by updating a comment and cleaning up a frontend Omit type. This PR completes the final cleanup for the "Remove Blast Propagation Information" project (ENG-2748). It updates a misleading comment in the `ec2-security-group` adapter and removes a redundant `'followOnlyBlastPropagation'` field from a frontend `Omit` type, aligning the codebase with the new AI-driven blast radius analysis. --- Linear Issue: [ENG-2748](https://linear.app/overmind/issue/ENG-2748/blast-propagation-removal-final-cleanup-comment-frontend-omit-docs)

Open in Web Open in Cursor 

--- > [!NOTE] > **Low Risk** > Comment-only Go change plus a TypeScript type cleanup; no behavior, auth, or data handling logic is modified. > > **Overview** > Removes stale, hardcoded blast-propagation references. > > Updates the `ec2-security-group` adapter comment to describe linking security groups to network interfaces for traversal to attached instances, and simplifies the frontend run-query helper types by no longer omitting `followOnlyBlastPropagation` from `Query_RecursionBehaviour`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 733dff3fc9dae5a58aa59ba94224942365109300. Configure [here](https://cursor.com/dashboard?tab=bugbot). Co-authored-by: Cursor Agent GitOrigin-RevId: 699a548231e5291544299fc2c67b0de72a89134f --- aws-source/adapters/ec2-security-group.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/aws-source/adapters/ec2-security-group.go b/aws-source/adapters/ec2-security-group.go index ee5db475..fe35ff1b 100644 --- a/aws-source/adapters/ec2-security-group.go +++ b/aws-source/adapters/ec2-security-group.go @@ -59,8 +59,7 @@ func securityGroupOutputMapper(_ context.Context, _ *ec2.Client, scope string, _ } // Network Interfaces using this security group - // This enables blast radius propagation from security groups to - // instances via their network interfaces + // Link to network interfaces using this security group so the graph and blast radius analysis can traverse to attached instances. if securityGroup.GroupId != nil { item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ From d0f3c1c485431a148fb9e6c405370ee93907775f Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Tue, 24 Feb 2026 18:11:04 +0000 Subject: [PATCH 09/74] Add Azure SQL Server Firewall Rule Client and Adapter (#3989) image > [!NOTE] > **Low Risk** > Mostly additive adapter/client code with new SDK client initialization; primary risk is incorrect scoping/query parsing causing missed or mislinked firewall rule items. > > **Overview** > Adds first-class discovery for Azure SQL Server firewall rules. > > Introduces a new `SqlServerFirewallRuleClient` (with generated mock) and a `NewSqlServerFirewallRule` searchable wrapper that supports `Get`/`Search`/`SearchStream`, emits stable composite IDs, and creates links to the parent SQL Server and referenced start/end IPs. > > Wires the new adapter into `manual/adapters.go` (including Azure SDK `FirewallRulesClient` initialization) and updates Azure resource ID path parsing (`GetResourceIDPathKeys`) to support `azure-sql-server-firewall-rule`, with unit tests covering paging and error cases. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 311ce443564a42650b4172b5ca5cfa9c15ebb752. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: b72ee0433b202eb7da5dbc6271cd5c5072b8e9be --- .../sql-server-firewall-rule-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + .../azure/manual/sql-server-firewall-rule.go | 250 +++++++++++++ .../manual/sql-server-firewall-rule_test.go | 330 ++++++++++++++++++ .../mock_sql_server_firewall_rule_client.go | 72 ++++ sources/azure/shared/utils.go | 1 + 6 files changed, 698 insertions(+) create mode 100644 sources/azure/clients/sql-server-firewall-rule-client.go create mode 100644 sources/azure/manual/sql-server-firewall-rule.go create mode 100644 sources/azure/manual/sql-server-firewall-rule_test.go create mode 100644 sources/azure/shared/mocks/mock_sql_server_firewall_rule_client.go diff --git a/sources/azure/clients/sql-server-firewall-rule-client.go b/sources/azure/clients/sql-server-firewall-rule-client.go new file mode 100644 index 00000000..aa7a8d30 --- /dev/null +++ b/sources/azure/clients/sql-server-firewall-rule-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" +) + +//go:generate mockgen -destination=../shared/mocks/mock_sql_server_firewall_rule_client.go -package=mocks -source=sql-server-firewall-rule-client.go + +// SqlServerFirewallRulePager is a type alias for the generic Pager interface with SQL server firewall rule response type. +type SqlServerFirewallRulePager = Pager[armsql.FirewallRulesClientListByServerResponse] + +// SqlServerFirewallRuleClient is an interface for interacting with Azure SQL server firewall rules. +type SqlServerFirewallRuleClient interface { + ListByServer(ctx context.Context, resourceGroupName string, serverName string) SqlServerFirewallRulePager + Get(ctx context.Context, resourceGroupName string, serverName string, firewallRuleName string) (armsql.FirewallRulesClientGetResponse, error) +} + +type sqlServerFirewallRuleClient struct { + client *armsql.FirewallRulesClient +} + +func (a *sqlServerFirewallRuleClient) ListByServer(ctx context.Context, resourceGroupName string, serverName string) SqlServerFirewallRulePager { + return a.client.NewListByServerPager(resourceGroupName, serverName, nil) +} + +func (a *sqlServerFirewallRuleClient) Get(ctx context.Context, resourceGroupName string, serverName string, firewallRuleName string) (armsql.FirewallRulesClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, serverName, firewallRuleName, nil) +} + +// NewSqlServerFirewallRuleClient creates a new SqlServerFirewallRuleClient from the Azure SDK client. +func NewSqlServerFirewallRuleClient(client *armsql.FirewallRulesClient) SqlServerFirewallRuleClient { + return &sqlServerFirewallRuleClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 88b9cc6a..8c82e193 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -195,6 +195,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create sql servers client: %w", err) } + sqlFirewallRulesClient, err := armsql.NewFirewallRulesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create sql firewall rules client: %w", err) + } + postgresqlFlexibleServersClient, err := armpostgresqlflexibleservers.NewServersClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create postgresql flexible servers client: %w", err) @@ -331,6 +336,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewSqlDatabasesClient(sqlDatabasesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewSqlServerFirewallRule( + clients.NewSqlServerFirewallRuleClient(sqlFirewallRulesClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewDocumentDBDatabaseAccounts( clients.NewDocumentDBDatabaseAccountsClient(documentDBDatabaseAccountsClient), resourceGroupScopes, @@ -492,6 +501,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewNetworkSubnet(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkNetworkInterface(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewSqlDatabase(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewSqlServerFirewallRule(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDocumentDBDatabaseAccounts(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewKeyVaultVault(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewKeyVaultManagedHSM(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/sql-server-firewall-rule.go b/sources/azure/manual/sql-server-firewall-rule.go new file mode 100644 index 00000000..2640c54b --- /dev/null +++ b/sources/azure/manual/sql-server-firewall-rule.go @@ -0,0 +1,250 @@ +package manual + +import ( + "context" + "errors" + + "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" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var SQLServerFirewallRuleLookupByName = shared.NewItemTypeLookup("name", azureshared.SQLServerFirewallRule) + +type sqlServerFirewallRuleWrapper struct { + client clients.SqlServerFirewallRuleClient + + *azureshared.MultiResourceGroupBase +} + +func NewSqlServerFirewallRule(client clients.SqlServerFirewallRuleClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &sqlServerFirewallRuleWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_DATABASE, + azureshared.SQLServerFirewallRule, + ), + } +} + +func (s sqlServerFirewallRuleWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: serverName and firewallRuleName", + Scope: scope, + ItemType: s.Type(), + } + } + serverName := queryParts[0] + firewallRuleName := queryParts[1] + if firewallRuleName == "" { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "firewallRuleName cannot be empty", + Scope: scope, + ItemType: s.Type(), + } + } + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + resp, err := s.client.Get(ctx, rgScope.ResourceGroup, serverName, firewallRuleName) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + return s.azureSqlServerFirewallRuleToSDPItem(&resp.FirewallRule, serverName, firewallRuleName, scope) +} + +func (s sqlServerFirewallRuleWrapper) azureSqlServerFirewallRuleToSDPItem(rule *armsql.FirewallRule, serverName, firewallRuleName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(rule, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(serverName, firewallRuleName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.SQLServerFirewallRule.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: nil, // FirewallRule has no Tags in the Azure SDK + } + + // Link to parent SQL Server (from resource ID or known server name) + if rule.ID != nil { + extractedServerName := azureshared.ExtractSQLServerNameFromDatabaseID(*rule.ID) + if extractedServerName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.SQLServer.String(), + Method: sdp.QueryMethod_GET, + Query: extractedServerName, + Scope: scope, + }, + }) + } + } else { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.SQLServer.String(), + Method: sdp.QueryMethod_GET, + Query: serverName, + Scope: scope, + }, + }) + } + + // Link to stdlib IP items for StartIPAddress and EndIPAddress (global scope, GET) + if rule.Properties != nil { + if rule.Properties.StartIPAddress != nil && *rule.Properties.StartIPAddress != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *rule.Properties.StartIPAddress, + Scope: "global", + }, + }) + } + if rule.Properties.EndIPAddress != nil && *rule.Properties.EndIPAddress != "" && (rule.Properties.StartIPAddress == nil || *rule.Properties.EndIPAddress != *rule.Properties.StartIPAddress) { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *rule.Properties.EndIPAddress, + Scope: "global", + }, + }) + } + } + + return sdpItem, nil +} + +func (s sqlServerFirewallRuleWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + SQLServerLookupByName, + SQLServerFirewallRuleLookupByName, + } +} + +func (s sqlServerFirewallRuleWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: serverName", + Scope: scope, + ItemType: s.Type(), + } + } + serverName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + pager := s.client.ListByServer(ctx, rgScope.ResourceGroup, serverName) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + for _, rule := range page.Value { + if rule.Name == nil { + continue + } + item, sdpErr := s.azureSqlServerFirewallRuleToSDPItem(rule, serverName, *rule.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (s sqlServerFirewallRuleWrapper) 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 _, rule := range page.Value { + if rule.Name == nil { + continue + } + item, sdpErr := s.azureSqlServerFirewallRuleToSDPItem(rule, serverName, *rule.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (s sqlServerFirewallRuleWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + SQLServerLookupByName, + }, + } +} + +func (s sqlServerFirewallRuleWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.SQLServer: true, + stdlib.NetworkIP: true, + } +} + +func (s sqlServerFirewallRuleWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_mssql_firewall_rule.id", + }, + } +} + +func (s sqlServerFirewallRuleWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Sql/servers/firewallRules/read", + } +} + +func (s sqlServerFirewallRuleWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/sql-server-firewall-rule_test.go b/sources/azure/manual/sql-server-firewall-rule_test.go new file mode 100644 index 00000000..e6ce9390 --- /dev/null +++ b/sources/azure/manual/sql-server-firewall-rule_test.go @@ -0,0 +1,330 @@ +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/sql/armsql/v2" + "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" +) + +type mockSqlServerFirewallRulePager struct { + pages []armsql.FirewallRulesClientListByServerResponse + index int +} + +func (m *mockSqlServerFirewallRulePager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockSqlServerFirewallRulePager) NextPage(ctx context.Context) (armsql.FirewallRulesClientListByServerResponse, error) { + if m.index >= len(m.pages) { + return armsql.FirewallRulesClientListByServerResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type errorSqlServerFirewallRulePager struct{} + +func (e *errorSqlServerFirewallRulePager) More() bool { + return true +} + +func (e *errorSqlServerFirewallRulePager) NextPage(ctx context.Context) (armsql.FirewallRulesClientListByServerResponse, error) { + return armsql.FirewallRulesClientListByServerResponse{}, errors.New("pager error") +} + +type testSqlServerFirewallRuleClient struct { + *mocks.MockSqlServerFirewallRuleClient + pager clients.SqlServerFirewallRulePager +} + +func (t *testSqlServerFirewallRuleClient) ListByServer(ctx context.Context, resourceGroupName, serverName string) clients.SqlServerFirewallRulePager { + return t.pager +} + +func TestSqlServerFirewallRule(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + serverName := "test-server" + firewallRuleName := "test-rule" + + t.Run("Get", func(t *testing.T) { + rule := createAzureSqlServerFirewallRule(serverName, firewallRuleName) + + mockClient := mocks.NewMockSqlServerFirewallRuleClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, firewallRuleName).Return( + armsql.FirewallRulesClientGetResponse{ + FirewallRule: *rule, + }, nil) + + wrapper := manual.NewSqlServerFirewallRule(&testSqlServerFirewallRuleClient{MockSqlServerFirewallRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, firewallRuleName) + 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.SQLServerFirewallRule.String() { + t.Errorf("Expected type %s, got %s", azureshared.SQLServerFirewallRule, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUniqueAttrValue := shared.CompositeLookupKey(serverName, firewallRuleName) + if sdpItem.UniqueAttributeValue() != expectedUniqueAttrValue { + t.Errorf("Expected unique attribute value %s, got %s", expectedUniqueAttrValue, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.SQLServer.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: serverName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, + { + ExpectedType: stdlib.NetworkIP.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "0.0.0.0", + ExpectedScope: "global", + }, + { + ExpectedType: stdlib.NetworkIP.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "255.255.255.255", + ExpectedScope: "global", + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSqlServerFirewallRuleClient(ctrl) + wrapper := manual.NewSqlServerFirewallRule(&testSqlServerFirewallRuleClient{MockSqlServerFirewallRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], serverName, true) + if qErr == nil { + t.Error("Expected error when providing only serverName (1 query part), but got nil") + } + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockSqlServerFirewallRuleClient(ctrl) + wrapper := manual.NewSqlServerFirewallRule(&testSqlServerFirewallRuleClient{MockSqlServerFirewallRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, "") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when firewall rule name is empty, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + rule1 := createAzureSqlServerFirewallRule(serverName, "rule1") + rule2 := createAzureSqlServerFirewallRule(serverName, "rule2") + + mockClient := mocks.NewMockSqlServerFirewallRuleClient(ctrl) + pager := &mockSqlServerFirewallRulePager{ + pages: []armsql.FirewallRulesClientListByServerResponse{ + { + FirewallRuleListResult: armsql.FirewallRuleListResult{ + Value: []*armsql.FirewallRule{rule1, rule2}, + }, + }, + }, + } + + testClient := &testSqlServerFirewallRuleClient{ + MockSqlServerFirewallRuleClient: mockClient, + pager: pager, + } + wrapper := manual.NewSqlServerFirewallRule(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") + } + + items, qErr := searchable.Search(ctx, wrapper.Scopes()[0], serverName, true) + if qErr != nil { + t.Fatalf("Expected no error from Search, got: %v", qErr) + } + if len(items) != 2 { + t.Errorf("Expected 2 items from Search, got %d", len(items)) + } + }) + + t.Run("SearchStream", func(t *testing.T) { + rule1 := createAzureSqlServerFirewallRule(serverName, "rule1") + + mockClient := mocks.NewMockSqlServerFirewallRuleClient(ctrl) + pager := &mockSqlServerFirewallRulePager{ + pages: []armsql.FirewallRulesClientListByServerResponse{ + { + FirewallRuleListResult: armsql.FirewallRuleListResult{ + Value: []*armsql.FirewallRule{rule1}, + }, + }, + }, + } + + testClient := &testSqlServerFirewallRuleClient{ + MockSqlServerFirewallRuleClient: mockClient, + pager: pager, + } + wrapper := manual.NewSqlServerFirewallRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchStreamable, ok := adapter.(discovery.SearchStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support SearchStream operation") + } + + stream := discovery.NewRecordingQueryResultStream() + searchStreamable.SearchStream(ctx, wrapper.Scopes()[0], serverName, true, stream) + items := stream.GetItems() + errs := stream.GetErrors() + if len(errs) > 0 { + t.Fatalf("Expected no errors from SearchStream, got: %v", errs) + } + if len(items) != 1 { + t.Errorf("Expected 1 item from SearchStream, got %d", len(items)) + } + }) + + t.Run("SearchWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSqlServerFirewallRuleClient(ctrl) + wrapper := manual.NewSqlServerFirewallRule(&testSqlServerFirewallRuleClient{MockSqlServerFirewallRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("firewall rule not found") + + mockClient := mocks.NewMockSqlServerFirewallRuleClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, "nonexistent-rule").Return( + armsql.FirewallRulesClientGetResponse{}, expectedErr) + + wrapper := manual.NewSqlServerFirewallRule(&testSqlServerFirewallRuleClient{MockSqlServerFirewallRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, "nonexistent-rule") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent firewall rule, but got nil") + } + }) + + t.Run("ErrorHandling_Search", func(t *testing.T) { + mockClient := mocks.NewMockSqlServerFirewallRuleClient(ctrl) + errorPager := &errorSqlServerFirewallRulePager{} + testClient := &testSqlServerFirewallRuleClient{ + MockSqlServerFirewallRuleClient: mockClient, + pager: errorPager, + } + + wrapper := manual.NewSqlServerFirewallRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0], serverName) + if qErr == nil { + t.Error("Expected error from Search when pager returns error, but got nil") + } + }) + + t.Run("InterfaceCompliance", func(t *testing.T) { + mockClient := mocks.NewMockSqlServerFirewallRuleClient(ctrl) + wrapper := manual.NewSqlServerFirewallRule(&testSqlServerFirewallRuleClient{MockSqlServerFirewallRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + w := wrapper.(sources.Wrapper) + + permissions := w.IAMPermissions() + if len(permissions) == 0 { + t.Error("Expected IAMPermissions to return at least one permission") + } + expectedPermission := "Microsoft.Sql/servers/firewallRules/read" + found := false + for _, perm := range permissions { + if perm == expectedPermission { + found = true + break + } + } + if !found { + t.Errorf("Expected IAMPermissions to include %s", expectedPermission) + } + + potentialLinks := w.PotentialLinks() + if !potentialLinks[azureshared.SQLServer] { + t.Error("Expected PotentialLinks to include SQLServer") + } + if !potentialLinks[stdlib.NetworkIP] { + t.Error("Expected PotentialLinks to include stdlib.NetworkIP") + } + + mappings := w.TerraformMappings() + if len(mappings) == 0 { + t.Error("Expected TerraformMappings to return at least one mapping") + } + foundMapping := false + for _, mapping := range mappings { + if mapping.GetTerraformQueryMap() == "azurerm_mssql_firewall_rule.id" { + foundMapping = true + break + } + } + if !foundMapping { + t.Error("Expected TerraformMappings to include 'azurerm_mssql_firewall_rule.id' mapping") + } + }) +} + +func createAzureSqlServerFirewallRule(serverName, firewallRuleName string) *armsql.FirewallRule { + ruleID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Sql/servers/" + serverName + "/firewallRules/" + firewallRuleName + return &armsql.FirewallRule{ + Name: to.Ptr(firewallRuleName), + ID: to.Ptr(ruleID), + Properties: &armsql.ServerFirewallRuleProperties{ + StartIPAddress: to.Ptr("0.0.0.0"), + EndIPAddress: to.Ptr("255.255.255.255"), + }, + } +} diff --git a/sources/azure/shared/mocks/mock_sql_server_firewall_rule_client.go b/sources/azure/shared/mocks/mock_sql_server_firewall_rule_client.go new file mode 100644 index 00000000..a25ffe4a --- /dev/null +++ b/sources/azure/shared/mocks/mock_sql_server_firewall_rule_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: sql-server-firewall-rule-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_sql_server_firewall_rule_client.go -package=mocks -source=sql-server-firewall-rule-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armsql "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockSqlServerFirewallRuleClient is a mock of SqlServerFirewallRuleClient interface. +type MockSqlServerFirewallRuleClient struct { + ctrl *gomock.Controller + recorder *MockSqlServerFirewallRuleClientMockRecorder + isgomock struct{} +} + +// MockSqlServerFirewallRuleClientMockRecorder is the mock recorder for MockSqlServerFirewallRuleClient. +type MockSqlServerFirewallRuleClientMockRecorder struct { + mock *MockSqlServerFirewallRuleClient +} + +// NewMockSqlServerFirewallRuleClient creates a new mock instance. +func NewMockSqlServerFirewallRuleClient(ctrl *gomock.Controller) *MockSqlServerFirewallRuleClient { + mock := &MockSqlServerFirewallRuleClient{ctrl: ctrl} + mock.recorder = &MockSqlServerFirewallRuleClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSqlServerFirewallRuleClient) EXPECT() *MockSqlServerFirewallRuleClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockSqlServerFirewallRuleClient) Get(ctx context.Context, resourceGroupName, serverName, firewallRuleName string) (armsql.FirewallRulesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, serverName, firewallRuleName) + ret0, _ := ret[0].(armsql.FirewallRulesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockSqlServerFirewallRuleClientMockRecorder) Get(ctx, resourceGroupName, serverName, firewallRuleName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSqlServerFirewallRuleClient)(nil).Get), ctx, resourceGroupName, serverName, firewallRuleName) +} + +// ListByServer mocks base method. +func (m *MockSqlServerFirewallRuleClient) ListByServer(ctx context.Context, resourceGroupName, serverName string) clients.SqlServerFirewallRulePager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByServer", ctx, resourceGroupName, serverName) + ret0, _ := ret[0].(clients.SqlServerFirewallRulePager) + return ret0 +} + +// ListByServer indicates an expected call of ListByServer. +func (mr *MockSqlServerFirewallRuleClientMockRecorder) ListByServer(ctx, resourceGroupName, serverName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByServer", reflect.TypeOf((*MockSqlServerFirewallRuleClient)(nil).ListByServer), ctx, resourceGroupName, serverName) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 3c08eeda..d1d51362 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -23,6 +23,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "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-sql-server-firewall-rule": {"servers", "firewallRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/firewallRules/{ruleName}", "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}", From 16336fb5e647cb3142b99539d3895548dac2af77 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Wed, 25 Feb 2026 16:41:46 +0100 Subject: [PATCH 10/74] [ENG-2763] Add GCP network tag relationship discovery (#4000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/user-attachments/assets/cfe3a528-b11f-4874-8cc7-6410c5c87638 ## Summary - Establish bidirectional links between GCP resources that share network tags (instances, firewalls, routes, instance templates) so blast radius is correct when tags or rules change - Add SEARCH-by-tag support to Compute Firewall, Compute Route (dynamic, list+filter), and Compute Instance (manual, aggregated list+filter) - Add `SearchFilterFunc` to the dynamic adapter framework for client-side post-filtering when the GCP API has no server-side tag filter ## Linear Ticket - **Ticket**: [ENG-2763](https://linear.app/overmind/issue/ENG-2763/implement-support-for-network-tag-relationships-eng-2757) — Implement support for network tag relationships (ENG-2757) - **Purpose**: Ensure blast radius correctly reflects network tag dependencies between instances, firewalls, and routes - **Related**: [ENG-2757](https://linear.app/overmind/issue/ENG-2757), [ENG-2756](https://linear.app/overmind/issue/ENG-2756) ## Changes - **`sources/gcp/shared/linker.go`**: Network tag detection (`isNetworkTag`) and SEARCH link emission in `AutoLink` for all four resource types - **`sources/gcp/shared/adapter-meta.go`**: New `SearchFilterFunc` type and field on `AdapterMeta` - **`sources/gcp/dynamic/adapter-searchable-listable.go`**: Apply `SearchFilterFunc` after list in `Search`; fallback to non-streaming in `SearchStream` when filter is set - **`sources/gcp/dynamic/adapters/compute-firewall.go`**: `SearchEndpointFunc`, `SearchFilterFunc` (targetTags/sourceTags), link rules for targetTags/sourceTags - **`sources/gcp/dynamic/adapters/compute-route.go`**: `SearchEndpointFunc`, `SearchFilterFunc` (tags), link rule for tags - **`sources/gcp/dynamic/adapters/compute-instance-template.go`**: Link rule for `properties.tags.items` - **`sources/gcp/manual/compute-instance.go`**: Emit SEARCH links for each network tag; add `Search`/`SearchLookups`/`Scopes` methods for tag-based SEARCH - **Tests**: Unit tests for linker network-tag handling, compute instance tag links, adapter type assertions; integration test (`network-tags_test.go`) ## Deviations from Approved Plan - **Instance template SEARCH resolution deferred (O9)**: As planned, instance templates do not implement SEARCH — only link emission via `properties.tags.items` - **Instance `Search` uses `List` with wildcard scope**: The plan called for `AggregatedList` in the `Search` method. The implementation delegates to `List(ctx, "*")` which internally uses `AggregatedList`, achieving the same result with less code duplication - **`SearchFilterFunc` on `AdapterMeta` instead of only `AdapterConfig`**: The filter is defined on `AdapterMeta` so it can be set declaratively alongside `SearchEndpointFunc` in adapter registration files, then plumbed through `AdapterConfig` — this is a minor structural choice not explicitly specified in the plan - No other material deviations from the approved plan --- > [!NOTE] > **Medium Risk** > Touches core discovery/linking and search execution paths (including cache/streaming fallbacks) and introduces list+filter tag searches that could affect performance or link correctness on large GCP projects. > > **Overview** > Adds *network tag relationship discovery* for GCP so resources that share tags (Compute Instances, Firewalls, Routes, and Instance Templates) emit bidirectional `SEARCH`-based links, improving blast radius accuracy. > > Implements tag-based `SEARCH` by listing then client-side filtering for dynamic adapters (`ComputeFirewall`, `ComputeRoute`) via a new `SearchFilterFunc` hook, plus a manual `ComputeInstance` `Search` implementation keyed by `networkTag`. Updates the linker (`AutoLink`) and dynamic potential-link calculation to recognize tag attribute keys and produce tag-driven `SEARCH` links, and adds unit + integration tests plus a small fix to the query engine to fall back from `SearchStream` to batch `Search` when streaming isn’t implemented. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 583a57977777df487cc3597fdb9d0406de0d1a80. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: cfa6af5ba89c93a32bd84050fd719e8e6d2fc904 --- .../dynamic/adapter-searchable-listable.go | 26 ++ sources/gcp/dynamic/adapter.go | 1 + sources/gcp/dynamic/adapters.go | 1 + .../gcp/dynamic/adapters/compute-firewall.go | 45 +++ .../adapters/compute-instance-template.go | 4 + sources/gcp/dynamic/adapters/compute-route.go | 21 ++ sources/gcp/dynamic/adapters_test.go | 8 +- sources/gcp/dynamic/shared.go | 13 +- .../integration-tests/network-tags_test.go | 342 ++++++++++++++++++ sources/gcp/manual/compute-instance.go | 84 +++++ sources/gcp/manual/compute-instance_test.go | 70 +++- sources/gcp/shared/adapter-meta.go | 9 + sources/gcp/shared/linker.go | 68 ++++ sources/gcp/shared/linker_test.go | 78 ++++ sources/transformer.go | 37 +- 15 files changed, 797 insertions(+), 10 deletions(-) create mode 100644 sources/gcp/integration-tests/network-tags_test.go diff --git a/sources/gcp/dynamic/adapter-searchable-listable.go b/sources/gcp/dynamic/adapter-searchable-listable.go index d439690e..509c2e0a 100644 --- a/sources/gcp/dynamic/adapter-searchable-listable.go +++ b/sources/gcp/dynamic/adapter-searchable-listable.go @@ -24,6 +24,7 @@ type SearchableListableDiscoveryAdapter interface { type SearchableListableAdapter struct { customSearchMethodDescription string searchEndpointFunc gcpshared.EndpointFunc + searchFilterFunc gcpshared.SearchFilterFunc ListableAdapter } @@ -32,6 +33,7 @@ func NewSearchableListableAdapter(searchURLFunc gcpshared.EndpointFunc, listEndp return SearchableListableAdapter{ customSearchMethodDescription: customSearchMethodDesc, searchEndpointFunc: searchURLFunc, + searchFilterFunc: config.SearchFilterFunc, ListableAdapter: ListableAdapter{ listEndpointFunc: listEndpointFunc, Adapter: Adapter{ @@ -131,6 +133,16 @@ func (g SearchableListableAdapter) Search(ctx context.Context, scope, query stri return nil, err } + if g.searchFilterFunc != nil { + filtered := make([]*sdp.Item, 0, len(items)) + for _, item := range items { + if g.searchFilterFunc(query, item) { + filtered = append(filtered, item) + } + } + items = filtered + } + if len(items) == 0 { // Cache not-found when no items were found notFoundErr := &sdp.QueryError{ @@ -153,6 +165,20 @@ func (g SearchableListableAdapter) Search(ctx context.Context, scope, query stri } func (g SearchableListableAdapter) SearchStream(ctx context.Context, scope, query string, ignoreCache bool, stream discovery.QueryResultStream) { + // When a post-filter is configured, fall back to the non-streaming Search + // so we can filter before sending items to the stream. + if g.searchFilterFunc != nil { + items, err := g.Search(ctx, scope, query, ignoreCache) + if err != nil { + stream.SendError(err) + return + } + for _, item := range items { + stream.SendItem(item) + } + return + } + location, err := g.validateScope(scope) if err != nil { stream.SendError(err) diff --git a/sources/gcp/dynamic/adapter.go b/sources/gcp/dynamic/adapter.go index f301d12a..9b920ec9 100644 --- a/sources/gcp/dynamic/adapter.go +++ b/sources/gcp/dynamic/adapter.go @@ -29,6 +29,7 @@ type AdapterConfig struct { IAMPermissions []string // List of IAM permissions required by the adapter NameSelector string // By default, it is `name`, but can be overridden for outlier cases ListResponseSelector string + SearchFilterFunc gcpshared.SearchFilterFunc } // Adapter implements discovery.ListableAdapter for GCP dynamic adapters. diff --git a/sources/gcp/dynamic/adapters.go b/sources/gcp/dynamic/adapters.go index 1011183a..03074ac9 100644 --- a/sources/gcp/dynamic/adapters.go +++ b/sources/gcp/dynamic/adapters.go @@ -135,6 +135,7 @@ func MakeAdapter(sdpItemType shared.ItemType, linker *gcpshared.Linker, httpCli IAMPermissions: meta.IAMPermissions, NameSelector: meta.NameSelector, ListResponseSelector: meta.ListResponseSelector, + SearchFilterFunc: meta.SearchFilterFunc, } switch adapterType(meta) { diff --git a/sources/gcp/dynamic/adapters/compute-firewall.go b/sources/gcp/dynamic/adapters/compute-firewall.go index 22aeaffc..02b70c11 100644 --- a/sources/gcp/dynamic/adapters/compute-firewall.go +++ b/sources/gcp/dynamic/adapters/compute-firewall.go @@ -1,6 +1,9 @@ package adapters import ( + "fmt" + "strings" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" ) @@ -19,6 +22,15 @@ var _ = registerableAdapter{ UniqueAttributeKeys: []string{"firewalls"}, IAMPermissions: []string{"compute.firewalls.get", "compute.firewalls.list"}, PredefinedRole: "roles/compute.viewer", + // Tag-based SEARCH: list all firewalls then filter by tag. + SearchEndpointFunc: func(query string, location gcpshared.LocationInfo) string { + if query == "" || strings.Contains(query, "/") { + return "" + } + return fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/firewalls", location.ProjectID) + }, + SearchDescription: "Search for firewalls by network tag. The query is a plain network tag name.", + SearchFilterFunc: firewallTagFilter, }, linkRules: map[string]*gcpshared.Impact{ "network": { @@ -27,6 +39,14 @@ var _ = registerableAdapter{ }, "sourceServiceAccounts": gcpshared.IAMServiceAccountImpactInOnly, "targetServiceAccounts": gcpshared.IAMServiceAccountImpactInOnly, + "targetTags": { + Description: "Firewall targets instances with this network tag. Changing the tag on either side affects which VMs the rule applies to.", + ToSDPItemType: gcpshared.ComputeInstance, + }, + "sourceTags": { + Description: "Firewall allows traffic from instances with this network tag. Changing the tag on either side affects traffic flow.", + ToSDPItemType: gcpshared.ComputeInstance, + }, }, terraformMapping: gcpshared.TerraformMapping{ Reference: "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_firewall", @@ -38,3 +58,28 @@ var _ = registerableAdapter{ }, }, }.Register() + +// firewallTagFilter keeps firewalls whose targetTags or sourceTags contain the query tag. +func firewallTagFilter(query string, item *sdp.Item) bool { + return itemAttributeContainsTag(item, "targetTags", query) || + itemAttributeContainsTag(item, "sourceTags", query) +} + +// itemAttributeContainsTag checks whether an item attribute (expected to be a +// list of strings) contains the given tag value. +func itemAttributeContainsTag(item *sdp.Item, attrKey, tag string) bool { + val, err := item.GetAttributes().Get(attrKey) + if err != nil { + return false + } + list, ok := val.([]interface{}) + if !ok { + return false + } + for _, elem := range list { + if s, ok := elem.(string); ok && s == tag { + return true + } + } + return false +} diff --git a/sources/gcp/dynamic/adapters/compute-instance-template.go b/sources/gcp/dynamic/adapters/compute-instance-template.go index 592cfecf..0743d00f 100644 --- a/sources/gcp/dynamic/adapters/compute-instance-template.go +++ b/sources/gcp/dynamic/adapters/compute-instance-template.go @@ -108,6 +108,10 @@ var _ = registerableAdapter{ 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, }, + "properties.tags.items": { + Description: "Network tag on the instance template. Changing this tag affects which firewall rules and routes apply to instances created from this template.", + ToSDPItemType: gcpshared.ComputeFirewall, + }, }, terraformMapping: gcpshared.TerraformMapping{ Reference: "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance_template", diff --git a/sources/gcp/dynamic/adapters/compute-route.go b/sources/gcp/dynamic/adapters/compute-route.go index 3a90e854..2d9206b9 100644 --- a/sources/gcp/dynamic/adapters/compute-route.go +++ b/sources/gcp/dynamic/adapters/compute-route.go @@ -1,6 +1,9 @@ package adapters import ( + "fmt" + "strings" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" @@ -19,6 +22,15 @@ var _ = registerableAdapter{ UniqueAttributeKeys: []string{"routes"}, IAMPermissions: []string{"compute.routes.get", "compute.routes.list"}, PredefinedRole: "roles/compute.viewer", + // Tag-based SEARCH: list all routes then filter by tag. + SearchEndpointFunc: func(query string, location gcpshared.LocationInfo) string { + if query == "" || strings.Contains(query, "/") { + return "" + } + return fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/routes", location.ProjectID) + }, + SearchDescription: "Search for routes by network tag. The query is a plain network tag name.", + SearchFilterFunc: routeTagFilter, }, linkRules: map[string]*gcpshared.Impact{ // https://cloud.google.com/compute/docs/reference/rest/v1/routes/get @@ -65,6 +77,10 @@ var _ = registerableAdapter{ 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, }, + "tags": { + Description: "Route applies to instances with this network tag. Changing the tag on either side affects which VMs the route targets.", + ToSDPItemType: gcpshared.ComputeInstance, + }, }, terraformMapping: gcpshared.TerraformMapping{ Reference: "https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_route", @@ -76,3 +92,8 @@ var _ = registerableAdapter{ }, }, }.Register() + +// routeTagFilter keeps routes whose tags array contains the query tag. +func routeTagFilter(query string, item *sdp.Item) bool { + return itemAttributeContainsTag(item, "tags", query) +} diff --git a/sources/gcp/dynamic/adapters_test.go b/sources/gcp/dynamic/adapters_test.go index 59ba8c50..cc56d16f 100644 --- a/sources/gcp/dynamic/adapters_test.go +++ b/sources/gcp/dynamic/adapters_test.go @@ -72,10 +72,16 @@ func Test_addAdapter(t *testing.T) { testCases := []testCase{ { name: "Listable adapter", - sdpType: gcpshared.ComputeFirewall, + sdpType: gcpshared.ComputeNetwork, locations: projectLocation, listable: true, }, + { + name: "SearchableListable adapter (firewall with tag search)", + sdpType: gcpshared.ComputeFirewall, + locations: projectLocation, + searchableListable: true, + }, { name: "Searchable adapter", sdpType: gcpshared.SQLAdminBackupRun, diff --git a/sources/gcp/dynamic/shared.go b/sources/gcp/dynamic/shared.go index b6ea6204..82d24eca 100644 --- a/sources/gcp/dynamic/shared.go +++ b/sources/gcp/dynamic/shared.go @@ -319,7 +319,7 @@ func externalCallMulti(ctx context.Context, itemsSelector string, httpCli *http. func potentialLinksFromLinkRules(itemType shared.ItemType, linkRules map[shared.ItemType]map[string]*gcpshared.Impact) []string { potentialLinksMap := make(map[string]bool) - for _, impact := range linkRules[itemType] { + for key, 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 @@ -328,6 +328,17 @@ func potentialLinksFromLinkRules(itemType shared.ItemType, linkRules map[shared. potentialLinksMap["ip"] = true potentialLinksMap["dns"] = true } + // Network tag keys produce additional links via AutoLink that aren't + // captured by ToSDPItemType alone. + if gcpshared.IsNetworkTagKey(key) { + switch itemType { + case gcpshared.ComputeFirewall, gcpshared.ComputeRoute: + potentialLinksMap[gcpshared.ComputeInstance.String()] = true + case gcpshared.ComputeInstance, gcpshared.ComputeInstanceTemplate: + potentialLinksMap[gcpshared.ComputeFirewall.String()] = true + potentialLinksMap[gcpshared.ComputeRoute.String()] = true + } + } } potentialLinks := make([]string, 0, len(potentialLinksMap)) diff --git a/sources/gcp/integration-tests/network-tags_test.go b/sources/gcp/integration-tests/network-tags_test.go new file mode 100644 index 00000000..f14af807 --- /dev/null +++ b/sources/gcp/integration-tests/network-tags_test.go @@ -0,0 +1,342 @@ +// Run commands (assumes RUN_GCP_INTEGRATION_TESTS, GCP_PROJECT_ID, GCP_ZONE are exported): +// +// All: go test ./sources/gcp/integration-tests/ -run "TestNetworkTagRelationships" -count 1 -v +// Setup: go test ./sources/gcp/integration-tests/ -run "TestNetworkTagRelationships/Setup" -count 1 -v +// Run: go test ./sources/gcp/integration-tests/ -run "TestNetworkTagRelationships/(Test|[A-Z])" -count 1 -v +// Teardown: go test ./sources/gcp/integration-tests/ -run "TestNetworkTagRelationships/Teardown" -count 1 -v +// +// Verify created resources with gcloud: +// +// gcloud compute instances describe integration-test-nettag-instance --zone=$GCP_ZONE --project=$GCP_PROJECT_ID --format="value(tags.items)" +// gcloud compute firewall-rules describe integration-test-nettag-fw --project=$GCP_PROJECT_ID --format="value(targetTags)" +// gcloud compute routes describe integration-test-nettag-route --project=$GCP_PROJECT_ID --format="value(tags)" + +package integrationtests + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "testing" + + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + "github.com/googleapis/gax-go/v2/apierror" + 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/gcp/dynamic" + "github.com/overmindtech/cli/sources/gcp/manual" + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" +) + +const ( + networkTagTestInstance = "integration-test-nettag-instance" + networkTagTestFirewall = "integration-test-nettag-fw" + networkTagTestRoute = "integration-test-nettag-route" + networkTag = "nettag-test" +) + +func TestNetworkTagRelationships(t *testing.T) { + projectID := os.Getenv("GCP_PROJECT_ID") + if projectID == "" { + t.Skip("GCP_PROJECT_ID environment variable not set") + } + + zone := os.Getenv("GCP_ZONE") + if zone == "" { + t.Skip("GCP_ZONE environment variable not set") + } + + t.Parallel() + + ctx := context.Background() + + instanceClient, err := compute.NewInstancesRESTClient(ctx) + if err != nil { + t.Fatalf("NewInstancesRESTClient: %v", err) + } + defer instanceClient.Close() + + firewallClient, err := compute.NewFirewallsRESTClient(ctx) + if err != nil { + t.Fatalf("NewFirewallsRESTClient: %v", err) + } + defer firewallClient.Close() + + routeClient, err := compute.NewRoutesRESTClient(ctx) + if err != nil { + t.Fatalf("NewRoutesRESTClient: %v", err) + } + defer routeClient.Close() + + // --- Setup --- + t.Run("Setup", func(t *testing.T) { + if err := createInstanceWithTags(ctx, instanceClient, projectID, zone); err != nil { + t.Fatalf("Failed to create tagged instance: %v", err) + } + if err := createFirewallWithTags(ctx, firewallClient, projectID); err != nil { + t.Fatalf("Failed to create tagged firewall: %v", err) + } + if err := createRouteWithTags(ctx, routeClient, projectID); err != nil { + t.Fatalf("Failed to create tagged route: %v", err) + } + }) + + // --- Run --- + t.Run("InstanceEmitsSearchLinksToFirewallAndRoute", func(t *testing.T) { + wrapper := manual.NewComputeInstance( + gcpshared.NewComputeInstanceClient(instanceClient), + []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}, + ) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], networkTagTestInstance, true) + if qErr != nil { + t.Fatalf("Get instance: %v", qErr) + } + + assertHasLinkedItemQuery(t, sdpItem, gcpshared.ComputeFirewall.String(), sdp.QueryMethod_SEARCH, networkTag, projectID) + assertHasLinkedItemQuery(t, sdpItem, gcpshared.ComputeRoute.String(), sdp.QueryMethod_SEARCH, networkTag, projectID) + }) + + t.Run("FirewallSearchByTagReturnsFirewall", func(t *testing.T) { + gcpHTTPCli, err := gcpshared.GCPHTTPClientWithOtel(ctx, "") + if err != nil { + t.Fatalf("GCPHTTPClientWithOtel: %v", err) + } + + adapter, err := dynamic.MakeAdapter(gcpshared.ComputeFirewall, gcpshared.NewLinker(), gcpHTTPCli, sdpcache.NewNoOpCache(), []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + if err != nil { + t.Fatalf("MakeAdapter: %v", err) + } + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Firewall adapter does not implement SearchableAdapter") + } + + items, qErr := searchable.Search(ctx, projectID, networkTag, true) + if qErr != nil { + t.Fatalf("Search: %v", qErr) + } + + found := false + for _, item := range items { + if v, err := item.GetAttributes().Get("name"); err == nil && v == networkTagTestFirewall { + found = true + break + } + } + if !found { + t.Errorf("Expected to find firewall %s in search results for tag %q, got %d items", networkTagTestFirewall, networkTag, len(items)) + } + }) + + t.Run("RouteSearchByTagReturnsRoute", func(t *testing.T) { + gcpHTTPCli, err := gcpshared.GCPHTTPClientWithOtel(ctx, "") + if err != nil { + t.Fatalf("GCPHTTPClientWithOtel: %v", err) + } + + adapter, err := dynamic.MakeAdapter(gcpshared.ComputeRoute, gcpshared.NewLinker(), gcpHTTPCli, sdpcache.NewNoOpCache(), []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + if err != nil { + t.Fatalf("MakeAdapter: %v", err) + } + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Route adapter does not implement SearchableAdapter") + } + + items, qErr := searchable.Search(ctx, projectID, networkTag, true) + if qErr != nil { + t.Fatalf("Search: %v", qErr) + } + + found := false + for _, item := range items { + if v, err := item.GetAttributes().Get("name"); err == nil && v == networkTagTestRoute { + found = true + break + } + } + if !found { + t.Errorf("Expected to find route %s in search results for tag %q, got %d items", networkTagTestRoute, networkTag, len(items)) + } + }) + + t.Run("FirewallEmitsSearchLinksToInstance", func(t *testing.T) { + gcpHTTPCli, err := gcpshared.GCPHTTPClientWithOtel(ctx, "") + if err != nil { + t.Fatalf("GCPHTTPClientWithOtel: %v", err) + } + + adapter, err := dynamic.MakeAdapter(gcpshared.ComputeFirewall, gcpshared.NewLinker(), gcpHTTPCli, sdpcache.NewNoOpCache(), []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + if err != nil { + t.Fatalf("MakeAdapter: %v", err) + } + + sdpItem, qErr := adapter.Get(ctx, projectID, networkTagTestFirewall, true) + if qErr != nil { + t.Fatalf("Get firewall: %v", qErr) + } + + assertHasLinkedItemQuery(t, sdpItem, gcpshared.ComputeInstance.String(), sdp.QueryMethod_SEARCH, networkTag, projectID) + }) + + // --- Teardown --- + t.Run("Teardown", func(t *testing.T) { + if err := deleteComputeInstance(ctx, instanceClient, projectID, zone, networkTagTestInstance); err != nil { + t.Errorf("Failed to delete instance: %v", err) + } + if err := deleteFirewall(ctx, firewallClient, projectID, networkTagTestFirewall); err != nil { + t.Errorf("Failed to delete firewall: %v", err) + } + if err := deleteRoute(ctx, routeClient, projectID, networkTagTestRoute); err != nil { + t.Errorf("Failed to delete route: %v", err) + } + }) +} + +func assertHasLinkedItemQuery(t *testing.T, item *sdp.Item, expectedType string, expectedMethod sdp.QueryMethod, expectedQuery, expectedScope string) { + t.Helper() + for _, liq := range item.GetLinkedItemQueries() { + q := liq.GetQuery() + if q.GetType() == expectedType && q.GetMethod() == expectedMethod && q.GetQuery() == expectedQuery && q.GetScope() == expectedScope { + return + } + } + t.Errorf("Missing LinkedItemQuery{type=%s, method=%s, query=%s, scope=%s} on item %s", + expectedType, expectedMethod, expectedQuery, expectedScope, item.UniqueAttributeValue()) +} + +// --- Resource creation/deletion helpers --- + +func createInstanceWithTags(ctx context.Context, client *compute.InstancesClient, projectID, zone string) error { + instance := &computepb.Instance{ + Name: ptr.To(networkTagTestInstance), + MachineType: ptr.To(fmt.Sprintf("zones/%s/machineTypes/e2-micro", zone)), + Tags: &computepb.Tags{ + Items: []string{networkTag}, + }, + Disks: []*computepb.AttachedDisk{ + { + Boot: ptr.To(true), + AutoDelete: ptr.To(true), + InitializeParams: &computepb.AttachedDiskInitializeParams{ + SourceImage: ptr.To("projects/debian-cloud/global/images/debian-12-bookworm-v20250415"), + DiskSizeGb: ptr.To(int64(10)), + }, + }, + }, + NetworkInterfaces: []*computepb.NetworkInterface{ + {StackType: ptr.To("IPV4_ONLY")}, + }, + } + + op, err := client.Insert(ctx, &computepb.InsertInstanceRequest{ + Project: projectID, + Zone: zone, + InstanceResource: instance, + }) + if err != nil { + var apiErr *apierror.APIError + if errors.As(err, &apiErr) && apiErr.HTTPCode() == http.StatusConflict { + log.Printf("Instance %s already exists, skipping", networkTagTestInstance) + return nil + } + return fmt.Errorf("insert instance: %w", err) + } + return op.Wait(ctx) +} + +func createFirewallWithTags(ctx context.Context, client *compute.FirewallsClient, projectID string) error { + fw := &computepb.Firewall{ + Name: ptr.To(networkTagTestFirewall), + Network: ptr.To(fmt.Sprintf("projects/%s/global/networks/default", projectID)), + TargetTags: []string{networkTag}, + Allowed: []*computepb.Allowed{ + { + IPProtocol: ptr.To("tcp"), + Ports: []string{"8080"}, + }, + }, + SourceRanges: []string{"0.0.0.0/0"}, + } + + op, err := client.Insert(ctx, &computepb.InsertFirewallRequest{ + Project: projectID, + FirewallResource: fw, + }) + if err != nil { + var apiErr *apierror.APIError + if errors.As(err, &apiErr) && apiErr.HTTPCode() == http.StatusConflict { + log.Printf("Firewall %s already exists, skipping", networkTagTestFirewall) + return nil + } + return fmt.Errorf("insert firewall: %w", err) + } + return op.Wait(ctx) +} + +func createRouteWithTags(ctx context.Context, client *compute.RoutesClient, projectID string) error { + route := &computepb.Route{ + Name: ptr.To(networkTagTestRoute), + Network: ptr.To(fmt.Sprintf("projects/%s/global/networks/default", projectID)), + DestRange: ptr.To("10.99.0.0/24"), + NextHopGateway: ptr.To(fmt.Sprintf("projects/%s/global/gateways/default-internet-gateway", projectID)), + Tags: []string{networkTag}, + Priority: ptr.To(uint32(900)), + } + + op, err := client.Insert(ctx, &computepb.InsertRouteRequest{ + Project: projectID, + RouteResource: route, + }) + if err != nil { + var apiErr *apierror.APIError + if errors.As(err, &apiErr) && apiErr.HTTPCode() == http.StatusConflict { + log.Printf("Route %s already exists, skipping", networkTagTestRoute) + return nil + } + return fmt.Errorf("insert route: %w", err) + } + return op.Wait(ctx) +} + +func deleteFirewall(ctx context.Context, client *compute.FirewallsClient, projectID, name string) error { + op, err := client.Delete(ctx, &computepb.DeleteFirewallRequest{ + Project: projectID, + Firewall: name, + }) + if err != nil { + var apiErr *apierror.APIError + if errors.As(err, &apiErr) && apiErr.HTTPCode() == http.StatusNotFound { + return nil + } + return fmt.Errorf("delete firewall: %w", err) + } + return op.Wait(ctx) +} + +func deleteRoute(ctx context.Context, client *compute.RoutesClient, projectID, name string) error { + op, err := client.Delete(ctx, &computepb.DeleteRouteRequest{ + Project: projectID, + Route: name, + }) + if err != nil { + var apiErr *apierror.APIError + if errors.As(err, &apiErr) && apiErr.HTTPCode() == http.StatusNotFound { + return nil + } + return fmt.Errorf("delete route: %w", err) + } + return op.Wait(ctx) +} diff --git a/sources/gcp/manual/compute-instance.go b/sources/gcp/manual/compute-instance.go index 240d3728..02863bc9 100644 --- a/sources/gcp/manual/compute-instance.go +++ b/sources/gcp/manual/compute-instance.go @@ -21,6 +21,7 @@ import ( ) var ComputeInstanceLookupByName = shared.NewItemTypeLookup("name", gcpshared.ComputeInstance) +var ComputeInstanceLookupByNetworkTag = shared.NewItemTypeLookup("networkTag", gcpshared.ComputeInstance) type computeInstanceWrapper struct { client gcpshared.ComputeInstanceClient @@ -67,6 +68,8 @@ func (c computeInstanceWrapper) PotentialLinks() map[shared.ItemType]bool { gcpshared.ComputeInstanceTemplate, gcpshared.ComputeRegionInstanceTemplate, gcpshared.ComputeInstanceGroupManager, + gcpshared.ComputeFirewall, + gcpshared.ComputeRoute, ) } @@ -92,6 +95,57 @@ func (c computeInstanceWrapper) SupportsWildcardScope() bool { return true } +func (c computeInstanceWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + {ComputeInstanceLookupByNetworkTag}, + } +} + +// Search finds compute instances by network tag. The engine routes +// project-scoped SEARCH queries to zonal scopes via substring matching, so +// scope is a zonal scope like "project.zone". We list all instances via +// AggregatedList and filter to the matching zone + tag. +func (c computeInstanceWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + tag := queryParts[0] + + allItems, qErr := c.List(ctx, "*") + if qErr != nil { + return nil, qErr + } + + var matched []*sdp.Item + for _, item := range allItems { + if item.GetScope() != scope { + continue + } + + tagsVal, err := item.GetAttributes().Get("tags") + if err != nil { + continue + } + tagsMap, ok := tagsVal.(map[string]interface{}) + if !ok { + continue + } + itemsVal, ok := tagsMap["items"] + if !ok { + continue + } + itemsList, ok := itemsVal.([]interface{}) + if !ok { + continue + } + for _, t := range itemsList { + if s, ok := t.(string); ok && s == tag { + matched = append(matched, item) + break + } + } + } + + return matched, nil +} + func (c computeInstanceWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { location, err := c.LocationFromScope(scope) if err != nil { @@ -598,6 +652,36 @@ func (c computeInstanceWrapper) gcpComputeInstanceToSDPItem(ctx context.Context, } } + // Link to firewalls and routes by network tag. + // Tag-based SEARCH lists all firewalls/routes in scope then filters; + // may be slow in very large projects. + if tags := instance.GetTags(); tags != nil { + for _, tag := range tags.GetItems() { + tag = strings.TrimSpace(tag) + if tag == "" { + continue + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, + &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.ComputeFirewall.String(), + Method: sdp.QueryMethod_SEARCH, + Query: tag, + Scope: location.ProjectID, + }, + }, + &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: gcpshared.ComputeRoute.String(), + Method: sdp.QueryMethod_SEARCH, + Query: tag, + Scope: location.ProjectID, + }, + }, + ) + } + } + // Set health based on status switch instance.GetStatus() { case computepb.Instance_RUNNING.String(): diff --git a/sources/gcp/manual/compute-instance_test.go b/sources/gcp/manual/compute-instance_test.go index f441a8e2..4b30387c 100644 --- a/sources/gcp/manual/compute-instance_test.go +++ b/sources/gcp/manual/compute-instance_test.go @@ -255,9 +255,9 @@ func TestComputeInstance(t *testing.T) { t.Fatalf("Expected 2 items, got: %d", len(items)) } - _, ok = adapter.(discovery.SearchStreamableAdapter) - if ok { - t.Fatalf("Adapter should not support SearchStream operation") + _, ok = adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Adapter should support Search operation (for network tag search)") } }) @@ -876,6 +876,70 @@ func TestComputeInstance(t *testing.T) { }) }) + t.Run("GetWithNetworkTags", func(t *testing.T) { + wrapper := manual.NewComputeInstance(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) + + instance := createComputeInstance("test-instance", computepb.Instance_RUNNING) + instance.Tags = &computepb.Tags{ + Items: []string{"web-server", "http-server"}, + } + + mockClient.EXPECT().Get(ctx, gomock.Any()).Return(instance, nil) + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-instance", true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + // Verify SEARCH links to ComputeFirewall and ComputeRoute for each tag + tagLinkTests := shared.QueryTests{ + { + ExpectedType: gcpshared.ComputeFirewall.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "web-server", + ExpectedScope: projectID, + }, + { + ExpectedType: gcpshared.ComputeRoute.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "web-server", + ExpectedScope: projectID, + }, + { + ExpectedType: gcpshared.ComputeFirewall.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "http-server", + ExpectedScope: projectID, + }, + { + ExpectedType: gcpshared.ComputeRoute.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "http-server", + ExpectedScope: projectID, + }, + } + + for _, qt := range tagLinkTests { + found := false + for _, liq := range sdpItem.GetLinkedItemQueries() { + q := liq.GetQuery() + if q.GetType() == qt.ExpectedType && + q.GetMethod() == qt.ExpectedMethod && + q.GetQuery() == qt.ExpectedQuery && + q.GetScope() == qt.ExpectedScope { + found = true + break + } + } + if !found { + t.Errorf("Missing LinkedItemQuery{type=%s, method=%s, query=%s, scope=%s}", + qt.ExpectedType, qt.ExpectedMethod, qt.ExpectedQuery, qt.ExpectedScope) + } + } + }) + t.Run("SupportsWildcardScope", func(t *testing.T) { wrapper := manual.NewComputeInstance(mockClient, []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) diff --git a/sources/gcp/shared/adapter-meta.go b/sources/gcp/shared/adapter-meta.go index 455ad73c..b2c24b37 100644 --- a/sources/gcp/shared/adapter-meta.go +++ b/sources/gcp/shared/adapter-meta.go @@ -8,6 +8,11 @@ import ( "github.com/overmindtech/cli/sources/shared" ) +// SearchFilterFunc filters items returned by SEARCH. Takes the search query +// and an SDP item; returns true to keep the item. Used for tag-based SEARCH +// where the GCP API does not support server-side filtering. +type SearchFilterFunc func(query string, item *sdp.Item) bool + // LocationLevel defines at which level of the GCP hierarchy a resource is located. type LocationLevel string @@ -48,6 +53,10 @@ type AdapterMeta struct { // However, there is an exception: https://cloud.google.com/dataproc/docs/reference/rest/v1/ListAutoscalingPoliciesResponse // Expected: `autoscalingPolicies` by convention, but the API returns `policies` ListResponseSelector string + // SearchFilterFunc, if set, is applied after listing items during SEARCH + // to keep only items matching the query. Used for tag-based SEARCH where + // the API has no server-side filter. + SearchFilterFunc SearchFilterFunc } // ============================================= diff --git a/sources/gcp/shared/linker.go b/sources/gcp/shared/linker.go index 719640b8..8881884f 100644 --- a/sources/gcp/shared/linker.go +++ b/sources/gcp/shared/linker.go @@ -43,6 +43,26 @@ func NewLinker() *Linker { } } +// networkTagKeys lists the attribute keys that carry GCP network tags. +var networkTagKeys = map[string]bool{ + "targetTags": true, + "sourceTags": true, + "tags": true, + "tags.items": true, + "properties.tags.items": true, +} + +// IsNetworkTagKey returns true when the key is a known network-tag attribute. +func IsNetworkTagKey(key string) bool { + return networkTagKeys[key] +} + +// isNetworkTag returns true when the key is a known network-tag attribute and +// the value looks like a plain tag (no "/" — not a resource URI). +func isNetworkTag(key, value string) bool { + return networkTagKeys[key] && !strings.Contains(value, "/") +} + // AutoLink tries to find the item type of the TO item based on its GCP resource name. // If the item type is identified, it links the FROM item to the TO item. func (l *Linker) AutoLink(ctx context.Context, projectID string, fromSDPItem *sdp.Item, fromSDPItemType shared.ItemType, toItemGCPResourceName string, keys []string) { @@ -61,6 +81,54 @@ func (l *Linker) AutoLink(ctx context.Context, projectID string, fromSDPItem *sd "ovm.gcp.key": key, } + // Network tag handling: detect plain tag values on known tag keys and + // emit SEARCH-based links instead of the normal resource-path flow. + if isNetworkTag(key, toItemGCPResourceName) { + tag := strings.TrimSpace(toItemGCPResourceName) + if tag == "" { + return // skip empty/whitespace-only tags (R2) + } + + switch fromSDPItemType { + case ComputeFirewall, ComputeRoute: + // Tag-based SEARCH lists all instances in scope then filters; + // may be slow in very large projects. + fromSDPItem.LinkedItemQueries = append(fromSDPItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: ComputeInstance.String(), + Method: sdp.QueryMethod_SEARCH, + Query: tag, + Scope: projectID, + }, + }) + case ComputeInstance, ComputeInstanceTemplate: + // Tag-based SEARCH lists all firewalls/routes in scope then filters; + // may be slow in very large projects. + fromSDPItem.LinkedItemQueries = append(fromSDPItem.LinkedItemQueries, + &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: ComputeFirewall.String(), + Method: sdp.QueryMethod_SEARCH, + Query: tag, + Scope: projectID, + }, + }, + &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: ComputeRoute.String(), + Method: sdp.QueryMethod_SEARCH, + Query: tag, + Scope: projectID, + }, + }, + ) + default: + log.WithContext(ctx).WithFields(lf).Debug("network tag on unexpected item type, skipping") + } + + return + } + impacts, ok := LinkRules[fromSDPItemType] if !ok { log.WithContext(ctx).WithFields(lf).Warnf("there are no link rules for the FROM item type") diff --git a/sources/gcp/shared/linker_test.go b/sources/gcp/shared/linker_test.go index 71806580..6fe15d47 100644 --- a/sources/gcp/shared/linker_test.go +++ b/sources/gcp/shared/linker_test.go @@ -226,6 +226,84 @@ func TestLinker_AutoLink(t *testing.T) { } } +func TestLinker_AutoLink_NetworkTags(t *testing.T) { + projectID := "my-project" + l := NewLinker() + + t.Run("Firewall targetTags → SEARCH ComputeInstance", func(t *testing.T) { + item := &sdp.Item{} + l.AutoLink(context.TODO(), projectID, item, ComputeFirewall, "web-server", []string{"targetTags"}) + + assertLinkedItemQuery(t, item, ComputeInstance.String(), sdp.QueryMethod_SEARCH, "web-server", projectID) + }) + + t.Run("Firewall sourceTags → SEARCH ComputeInstance", func(t *testing.T) { + item := &sdp.Item{} + l.AutoLink(context.TODO(), projectID, item, ComputeFirewall, "nat-gateway", []string{"sourceTags"}) + + assertLinkedItemQuery(t, item, ComputeInstance.String(), sdp.QueryMethod_SEARCH, "nat-gateway", projectID) + }) + + t.Run("Route tags → SEARCH ComputeInstance", func(t *testing.T) { + item := &sdp.Item{} + l.AutoLink(context.TODO(), projectID, item, ComputeRoute, "backend", []string{"tags"}) + + assertLinkedItemQuery(t, item, ComputeInstance.String(), sdp.QueryMethod_SEARCH, "backend", projectID) + }) + + t.Run("Instance template tags.items → SEARCH ComputeFirewall and ComputeRoute", func(t *testing.T) { + item := &sdp.Item{} + l.AutoLink(context.TODO(), projectID, item, ComputeInstanceTemplate, "http-server", []string{"properties", "tags", "items"}) + + if len(item.GetLinkedItemQueries()) != 2 { + t.Fatalf("expected 2 linked item queries, got %d", len(item.GetLinkedItemQueries())) + } + + assertLinkedItemQuery(t, item, ComputeFirewall.String(), sdp.QueryMethod_SEARCH, "http-server", projectID) + + q2 := item.GetLinkedItemQueries()[1].GetQuery() + if q2.GetType() != ComputeRoute.String() { + t.Errorf("second query type = %s, want %s", q2.GetType(), ComputeRoute.String()) + } + if q2.GetMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("second query method = %s, want SEARCH", q2.GetMethod()) + } + }) + + t.Run("Empty tag is skipped", func(t *testing.T) { + item := &sdp.Item{} + l.AutoLink(context.TODO(), projectID, item, ComputeFirewall, " ", []string{"targetTags"}) + + if len(item.GetLinkedItemQueries()) != 0 { + t.Fatalf("expected 0 linked item queries for empty tag, got %d", len(item.GetLinkedItemQueries())) + } + }) + + t.Run("URI value on tag key falls through to normal linking", func(t *testing.T) { + item := &sdp.Item{} + l.AutoLink(context.TODO(), projectID, item, ComputeRoute, "projects/my-project/zones/us-central1-a/instances/my-vm", []string{"tags"}) + + // Should NOT be treated as network tag (contains /), falls through to normal link rules + for _, liq := range item.GetLinkedItemQueries() { + if liq.GetQuery().GetMethod() == sdp.QueryMethod_SEARCH && liq.GetQuery().GetType() == ComputeInstance.String() && liq.GetQuery().GetQuery() == "projects/my-project/zones/us-central1-a/instances/my-vm" { + t.Error("URI value on tag key should not produce a network-tag SEARCH link") + } + } + }) +} + +func assertLinkedItemQuery(t *testing.T, item *sdp.Item, expectedType string, expectedMethod sdp.QueryMethod, expectedQuery, expectedScope string) { + t.Helper() + for _, liq := range item.GetLinkedItemQueries() { + q := liq.GetQuery() + if q.GetType() == expectedType && q.GetMethod() == expectedMethod && q.GetQuery() == expectedQuery && q.GetScope() == expectedScope { + return + } + } + t.Errorf("did not find LinkedItemQuery{type=%s, method=%s, query=%s, scope=%s} in %d queries", + expectedType, expectedMethod, expectedQuery, expectedScope, len(item.GetLinkedItemQueries())) +} + func Test_determineScope(t *testing.T) { type args struct { ctx context.Context diff --git a/sources/transformer.go b/sources/transformer.go index 92c9f0d0..d7617c28 100644 --- a/sources/transformer.go +++ b/sources/transformer.go @@ -858,11 +858,6 @@ func (s *standardSearchableAdapterImpl) SearchStream(ctx context.Context, scope return } - if s.searchStreamable == nil { - log.WithField("adapter", s.Name()).Debug("search stream operation not supported") - return - } - // This must be a regular query in the format of: // {{datasetName}}|{{tableName}} queryParts = strings.Split(query, shared.QuerySeparator) @@ -889,6 +884,38 @@ func (s *standardSearchableAdapterImpl) SearchStream(ctx context.Context, scope return } + if s.searchStreamable == nil { + // No streaming implementation; fall back to the batch Search method + // and send items individually. Without this, wrappers that implement + // SearchableWrapper but not SearchStreamableWrapper would silently + // return zero items because the engine always prefers SearchStream. + items, qErr := s.searchable.Search(ctx, scope, queryParts...) + if qErr != nil { + if IsNotFound(qErr) { + s.cache.StoreError(ctx, qErr, shared.DefaultCacheDuration, ck) + } + stream.SendError(qErr) + return + } + if len(items) == 0 { + 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 + } + for _, item := range items { + s.cache.StoreItem(ctx, item, shared.DefaultCacheDuration, ck) + stream.SendItem(item) + } + return + } + s.searchStreamable.SearchStream(ctx, stream, s.cache, ck, scope, queryParts...) } From e3e9c2a881cdacf7b9fd82bcf06beb9103c67065 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:16:39 +0000 Subject: [PATCH 11/74] Add Azure Private Endpoint Client and Adapter (#3998) image --- > [!NOTE] > **Medium Risk** > Moderate risk due to the `armnetwork` major-version bump and new network adapter wiring, which could cause compile/runtime incompatibilities with Azure SDK types and discovery link behavior. > > **Overview** > Adds discovery support for Azure **Private Endpoints** by introducing a new `PrivateEndpointsClient` wrapper and a `network-private-endpoint` manual adapter (List/Get/streaming) that emits linked queries to related subnet/VNet, NICs, application security groups, private link services, and stdlib IP/DNS entries. > > Upgrades the Azure Network SDK dependency from `armnetwork/v8` to `armnetwork/v9` across clients, adapters, tests, and generated mocks, and registers the new private endpoint adapter + SDK client in `manual/adapters.go`. Documentation in the cursor skill guide is also updated to emphasize mandatory IP/DNS linking in nested/array fields. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 15a102d915ff606e8cda352926da3d4125961022. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 1c18e8ff0caa8df91287d0387873d0038a36ca0c --- go.mod | 2 +- go.sum | 4 +- .../clients/application-gateways-client.go | 2 +- .../azure/clients/load-balancers-client.go | 2 +- .../clients/network-interfaces-client.go | 2 +- .../network-private-endpoint-client.go | 35 ++ .../clients/network-security-groups-client.go | 2 +- sources/azure/clients/public-ip-addresses.go | 2 +- sources/azure/clients/route-tables-client.go | 2 +- sources/azure/clients/subnets-client.go | 2 +- .../azure/clients/virtual-networks-client.go | 2 +- .../compute-availability-set_test.go | 2 +- .../compute-virtual-machine-extension_test.go | 2 +- ...ompute-virtual-machine-run-command_test.go | 2 +- .../compute-virtual-machine-scale-set_test.go | 2 +- .../compute-virtual-machine_test.go | 2 +- .../network-application-gateway_test.go | 2 +- .../network-load-balancer_test.go | 2 +- .../network-network-interface_test.go | 2 +- .../network-network-security-group_test.go | 2 +- .../network-public-ip-address_test.go | 2 +- .../network-route-table_test.go | 2 +- .../network-virtual-network_test.go | 2 +- sources/azure/manual/adapters.go | 14 +- .../manual/network-application-gateway.go | 2 +- .../network-application-gateway_test.go | 2 +- sources/azure/manual/network-load-balancer.go | 2 +- .../manual/network-load-balancer_test.go | 2 +- .../azure/manual/network-network-interface.go | 2 +- .../manual/network-network-interface_test.go | 2 +- .../manual/network-network-security-group.go | 2 +- .../network-network-security-group_test.go | 2 +- .../azure/manual/network-private-endpoint.go | 346 ++++++++++++++++++ .../manual/network-private-endpoint_test.go | 334 +++++++++++++++++ .../azure/manual/network-public-ip-address.go | 2 +- .../manual/network-public-ip-address_test.go | 2 +- sources/azure/manual/network-route-table.go | 2 +- .../azure/manual/network-route-table_test.go | 2 +- sources/azure/manual/network-subnet.go | 2 +- sources/azure/manual/network-subnet_test.go | 2 +- .../azure/manual/network-virtual-network.go | 2 +- .../manual/network-virtual-network_test.go | 2 +- .../mocks/mock_application_gateways_client.go | 2 +- .../mocks/mock_load_balancers_client.go | 2 +- .../mocks/mock_network_interfaces_client.go | 2 +- .../mock_network_private_endpoint_client.go | 72 ++++ .../mock_network_security_groups_client.go | 2 +- .../mocks/mock_public_ip_addresses_client.go | 2 +- .../shared/mocks/mock_route_tables_client.go | 2 +- .../azure/shared/mocks/mock_subnets_client.go | 2 +- .../mocks/mock_virtual_networks_client.go | 2 +- 51 files changed, 846 insertions(+), 49 deletions(-) create mode 100644 sources/azure/clients/network-private-endpoint-client.go create mode 100644 sources/azure/manual/network-private-endpoint.go create mode 100644 sources/azure/manual/network-private-endpoint_test.go create mode 100644 sources/azure/shared/mocks/mock_network_private_endpoint_client.go diff --git a/go.mod b/go.mod index 41a612a1..1e238062 100644 --- a/go.mod +++ b/go.mod @@ -50,7 +50,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2 v2.0.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8 v8.0.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9 v9.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5 v5.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2 v2.1.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2 v2.0.0-beta.7 diff --git a/go.sum b/go.sum index 9dbf56cc..2398131b 100644 --- a/go.sum +++ b/go.sum @@ -130,8 +130,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanage github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.2.0/go.mod h1:8wzvopPfyZYPaQUoKW87Zfdul7jmJMDfp/k7YY3oJyA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 h1:L7G3dExHBgUxsO3qpTGhk/P2dgnYyW48yn7AO33Tbek= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0/go.mod h1:Ms6gYEy0+A2knfKrwdatsggTXYA2+ICKug8w7STorFw= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8 v8.0.0 h1:7QO7GhGat25QEYL4h607O9zNNTUlAv8PbSesW6Ol5Gg= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8 v8.0.0/go.mod h1:mCqeYzwyjn/pw0JVqHJMIzfUQJrlcV0YjTg5b0NK+F0= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9 v9.0.0 h1:CbHDMVJhcJSmXenq+UDWyIjumzVkZIb5pVUGzsCok5M= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9 v9.0.0/go.mod h1:raqbEXrok4aycS74XoU6p9Hne1dliAFpHLizlp+qJoM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5 v5.0.0 h1:S7K+MLPEYe+g9AX9dLKldBpYV03bPl7zeDaWhiNDqqs= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5 v5.0.0/go.mod h1:EHRrmrnS2Q8fB3+DE30TTk04JLqjui5ZJEF7eMVQ2/M= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= diff --git a/sources/azure/clients/application-gateways-client.go b/sources/azure/clients/application-gateways-client.go index e755f3dd..6ba0d285 100644 --- a/sources/azure/clients/application-gateways-client.go +++ b/sources/azure/clients/application-gateways-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" ) //go:generate mockgen -destination=../shared/mocks/mock_application_gateways_client.go -package=mocks -source=application-gateways-client.go diff --git a/sources/azure/clients/load-balancers-client.go b/sources/azure/clients/load-balancers-client.go index 2ba33b1e..2f3e5df3 100644 --- a/sources/azure/clients/load-balancers-client.go +++ b/sources/azure/clients/load-balancers-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" ) //go:generate mockgen -destination=../shared/mocks/mock_load_balancers_client.go -package=mocks -source=load-balancers-client.go diff --git a/sources/azure/clients/network-interfaces-client.go b/sources/azure/clients/network-interfaces-client.go index cbc8963c..ad9a50a3 100644 --- a/sources/azure/clients/network-interfaces-client.go +++ b/sources/azure/clients/network-interfaces-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" ) //go:generate mockgen -destination=../shared/mocks/mock_network_interfaces_client.go -package=mocks -source=network-interfaces-client.go diff --git a/sources/azure/clients/network-private-endpoint-client.go b/sources/azure/clients/network-private-endpoint-client.go new file mode 100644 index 00000000..701eaa83 --- /dev/null +++ b/sources/azure/clients/network-private-endpoint-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" +) + +//go:generate mockgen -destination=../shared/mocks/mock_network_private_endpoint_client.go -package=mocks -source=network-private-endpoint-client.go + +// PrivateEndpointsPager is a type alias for the generic Pager interface with private endpoint response type. +type PrivateEndpointsPager = Pager[armnetwork.PrivateEndpointsClientListResponse] + +// PrivateEndpointsClient is an interface for interacting with Azure private endpoints. +type PrivateEndpointsClient interface { + Get(ctx context.Context, resourceGroupName string, privateEndpointName string) (armnetwork.PrivateEndpointsClientGetResponse, error) + List(resourceGroupName string) PrivateEndpointsPager +} + +type privateEndpointsClient struct { + client *armnetwork.PrivateEndpointsClient +} + +func (c *privateEndpointsClient) Get(ctx context.Context, resourceGroupName string, privateEndpointName string) (armnetwork.PrivateEndpointsClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, privateEndpointName, nil) +} + +func (c *privateEndpointsClient) List(resourceGroupName string) PrivateEndpointsPager { + return c.client.NewListPager(resourceGroupName, nil) +} + +// NewPrivateEndpointsClient creates a new PrivateEndpointsClient from the Azure SDK client. +func NewPrivateEndpointsClient(client *armnetwork.PrivateEndpointsClient) PrivateEndpointsClient { + return &privateEndpointsClient{client: client} +} diff --git a/sources/azure/clients/network-security-groups-client.go b/sources/azure/clients/network-security-groups-client.go index 7dc9df0e..fac29fc0 100644 --- a/sources/azure/clients/network-security-groups-client.go +++ b/sources/azure/clients/network-security-groups-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" ) //go:generate mockgen -destination=../shared/mocks/mock_network_security_groups_client.go -package=mocks -source=network-security-groups-client.go diff --git a/sources/azure/clients/public-ip-addresses.go b/sources/azure/clients/public-ip-addresses.go index c5b29958..a43e9b06 100644 --- a/sources/azure/clients/public-ip-addresses.go +++ b/sources/azure/clients/public-ip-addresses.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" ) //go:generate mockgen -destination=../shared/mocks/mock_public_ip_addresses_client.go -package=mocks -source=public-ip-addresses.go diff --git a/sources/azure/clients/route-tables-client.go b/sources/azure/clients/route-tables-client.go index 2708b3c3..5686be77 100644 --- a/sources/azure/clients/route-tables-client.go +++ b/sources/azure/clients/route-tables-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" ) //go:generate mockgen -destination=../shared/mocks/mock_route_tables_client.go -package=mocks -source=route-tables-client.go diff --git a/sources/azure/clients/subnets-client.go b/sources/azure/clients/subnets-client.go index 1f3dfce0..385e83e0 100644 --- a/sources/azure/clients/subnets-client.go +++ b/sources/azure/clients/subnets-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" ) //go:generate mockgen -destination=../shared/mocks/mock_subnets_client.go -package=mocks -source=subnets-client.go diff --git a/sources/azure/clients/virtual-networks-client.go b/sources/azure/clients/virtual-networks-client.go index 07e99710..4b8616f6 100644 --- a/sources/azure/clients/virtual-networks-client.go +++ b/sources/azure/clients/virtual-networks-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" ) //go:generate mockgen -destination=../shared/mocks/mock_virtual_networks_client.go -package=mocks -source=virtual-networks-client.go diff --git a/sources/azure/integration-tests/compute-availability-set_test.go b/sources/azure/integration-tests/compute-availability-set_test.go index ffc52e92..fc7cde62 100644 --- a/sources/azure/integration-tests/compute-availability-set_test.go +++ b/sources/azure/integration-tests/compute-availability-set_test.go @@ -11,7 +11,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/compute-virtual-machine-extension_test.go b/sources/azure/integration-tests/compute-virtual-machine-extension_test.go index 1c89a6df..89f45ec1 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-extension_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-extension_test.go @@ -11,7 +11,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go b/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go index e887ac32..9b18c877 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-run-command_test.go @@ -11,7 +11,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go b/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go index f71515fc..f7a723aa 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-scale-set_test.go @@ -12,7 +12,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/compute-virtual-machine_test.go b/sources/azure/integration-tests/compute-virtual-machine_test.go index 2ef716e8..0b78f1bb 100644 --- a/sources/azure/integration-tests/compute-virtual-machine_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine_test.go @@ -11,7 +11,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/network-application-gateway_test.go b/sources/azure/integration-tests/network-application-gateway_test.go index 7525eee2..a6e6e899 100644 --- a/sources/azure/integration-tests/network-application-gateway_test.go +++ b/sources/azure/integration-tests/network-application-gateway_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/network-load-balancer_test.go b/sources/azure/integration-tests/network-load-balancer_test.go index b6cd87ba..cd23793c 100644 --- a/sources/azure/integration-tests/network-load-balancer_test.go +++ b/sources/azure/integration-tests/network-load-balancer_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/network-network-interface_test.go b/sources/azure/integration-tests/network-network-interface_test.go index a496409b..8e7c483d 100644 --- a/sources/azure/integration-tests/network-network-interface_test.go +++ b/sources/azure/integration-tests/network-network-interface_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/network-network-security-group_test.go b/sources/azure/integration-tests/network-network-security-group_test.go index 4618167b..08de4270 100644 --- a/sources/azure/integration-tests/network-network-security-group_test.go +++ b/sources/azure/integration-tests/network-network-security-group_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/network-public-ip-address_test.go b/sources/azure/integration-tests/network-public-ip-address_test.go index 9fea6d49..f2eeab78 100644 --- a/sources/azure/integration-tests/network-public-ip-address_test.go +++ b/sources/azure/integration-tests/network-public-ip-address_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/network-route-table_test.go b/sources/azure/integration-tests/network-route-table_test.go index 1de639d7..f9342ef4 100644 --- a/sources/azure/integration-tests/network-route-table_test.go +++ b/sources/azure/integration-tests/network-route-table_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" "k8s.io/utils/ptr" diff --git a/sources/azure/integration-tests/network-virtual-network_test.go b/sources/azure/integration-tests/network-virtual-network_test.go index 9a831b64..7a155c81 100644 --- a/sources/azure/integration-tests/network-virtual-network_test.go +++ b/sources/azure/integration-tests/network-virtual-network_test.go @@ -5,7 +5,7 @@ import ( "os" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 8c82e193..6fc5f87c 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -12,7 +12,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" @@ -151,6 +151,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create load balancers client: %w", err) } + privateEndpointsClient, err := armnetwork.NewPrivateEndpointsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create private endpoints client: %w", err) + } + batchAccountsClient, err := armbatch.NewAccountClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create batch accounts client: %w", err) @@ -364,6 +369,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewLoadBalancersClient(loadBalancersClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewNetworkPrivateEndpoint( + clients.NewPrivateEndpointsClient(privateEndpointsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewNetworkZone( clients.NewZonesClient(zonesClient), resourceGroupScopes, @@ -468,7 +477,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewSnapshotsClient(snapshotsClient), resourceGroupScopes, ), cache), - ) + ) } // Subscription-scoped adapters (not resource-group-scoped) @@ -535,6 +544,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewComputeGalleryImage(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeSnapshot(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeSharedGalleryImage(nil, subscriptionID), noOpCache), + sources.WrapperToAdapter(NewNetworkPrivateEndpoint(nil, placeholderResourceGroupScopes), noOpCache), ) _ = regions diff --git a/sources/azure/manual/network-application-gateway.go b/sources/azure/manual/network-application-gateway.go index a4511f87..97fd89cf 100644 --- a/sources/azure/manual/network-application-gateway.go +++ b/sources/azure/manual/network-application-gateway.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/go/sdpcache" diff --git a/sources/azure/manual/network-application-gateway_test.go b/sources/azure/manual/network-application-gateway_test.go index 17bd4d56..d69e14ea 100644 --- a/sources/azure/manual/network-application-gateway_test.go +++ b/sources/azure/manual/network-application-gateway_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/go/discovery" diff --git a/sources/azure/manual/network-load-balancer.go b/sources/azure/manual/network-load-balancer.go index 4369d479..3b3081dd 100644 --- a/sources/azure/manual/network-load-balancer.go +++ b/sources/azure/manual/network-load-balancer.go @@ -6,7 +6,7 @@ import ( "fmt" "strings" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/go/sdpcache" diff --git a/sources/azure/manual/network-load-balancer_test.go b/sources/azure/manual/network-load-balancer_test.go index e20ead69..a7396166 100644 --- a/sources/azure/manual/network-load-balancer_test.go +++ b/sources/azure/manual/network-load-balancer_test.go @@ -8,7 +8,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/go/discovery" diff --git a/sources/azure/manual/network-network-interface.go b/sources/azure/manual/network-network-interface.go index 7c7d75e9..c8f52a0b 100644 --- a/sources/azure/manual/network-network-interface.go +++ b/sources/azure/manual/network-network-interface.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/go/sdpcache" diff --git a/sources/azure/manual/network-network-interface_test.go b/sources/azure/manual/network-network-interface_test.go index a7f632c5..dd23559a 100644 --- a/sources/azure/manual/network-network-interface_test.go +++ b/sources/azure/manual/network-network-interface_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/go/discovery" diff --git a/sources/azure/manual/network-network-security-group.go b/sources/azure/manual/network-network-security-group.go index 5e072ab1..1a6cfe95 100644 --- a/sources/azure/manual/network-network-security-group.go +++ b/sources/azure/manual/network-network-security-group.go @@ -6,7 +6,7 @@ import ( "net" "strings" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/go/sdpcache" diff --git a/sources/azure/manual/network-network-security-group_test.go b/sources/azure/manual/network-network-security-group_test.go index e79071fd..72f37f56 100644 --- a/sources/azure/manual/network-network-security-group_test.go +++ b/sources/azure/manual/network-network-security-group_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/go/discovery" diff --git a/sources/azure/manual/network-private-endpoint.go b/sources/azure/manual/network-private-endpoint.go new file mode 100644 index 00000000..b4abcbe8 --- /dev/null +++ b/sources/azure/manual/network-private-endpoint.go @@ -0,0 +1,346 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "github.com/overmindtech/cli/go/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 NetworkPrivateEndpointLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkPrivateEndpoint) + +type networkPrivateEndpointWrapper struct { + client clients.PrivateEndpointsClient + + *azureshared.MultiResourceGroupBase +} + +func NewNetworkPrivateEndpoint(client clients.PrivateEndpointsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &networkPrivateEndpointWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + azureshared.NetworkPrivateEndpoint, + ), + } +} + +func (n networkPrivateEndpointWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + pager := n.client.List(rgScope.ResourceGroup) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + for _, pe := range page.Value { + if pe.Name == nil { + continue + } + item, sdpErr := n.azurePrivateEndpointToSDPItem(pe, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (n networkPrivateEndpointWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + pager := n.client.List(rgScope.ResourceGroup) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + for _, pe := range page.Value { + if pe.Name == nil { + continue + } + item, sdpErr := n.azurePrivateEndpointToSDPItem(pe, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (n networkPrivateEndpointWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("query must be a private endpoint name"), scope, n.Type()) + } + name := queryParts[0] + if name == "" { + return nil, azureshared.QueryError(errors.New("private endpoint name cannot be empty"), scope, n.Type()) + } + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + resp, err := n.client.Get(ctx, rgScope.ResourceGroup, name) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + return n.azurePrivateEndpointToSDPItem(&resp.PrivateEndpoint, scope) +} + +func (n networkPrivateEndpointWrapper) azurePrivateEndpointToSDPItem(pe *armnetwork.PrivateEndpoint, scope string) (*sdp.Item, *sdp.QueryError) { + if pe.Name == nil { + return nil, azureshared.QueryError(errors.New("private endpoint name is nil"), scope, n.Type()) + } + attributes, err := shared.ToAttributesWithExclude(pe, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.NetworkPrivateEndpoint.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(pe.Tags), + } + + // Health status from ProvisioningState + if pe.Properties != nil && pe.Properties.ProvisioningState != nil { + switch *pe.Properties.ProvisioningState { + case armnetwork.ProvisioningStateSucceeded: + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case armnetwork.ProvisioningStateCreating, armnetwork.ProvisioningStateUpdating, armnetwork.ProvisioningStateDeleting: + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case armnetwork.ProvisioningStateFailed, armnetwork.ProvisioningStateCanceled: + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + } + } + + // Link to Subnet and parent VirtualNetwork + if pe.Properties != nil && pe.Properties.Subnet != nil && pe.Properties.Subnet.ID != nil { + subnetParams := azureshared.ExtractPathParamsFromResourceID(*pe.Properties.Subnet.ID, []string{"virtualNetworks", "subnets"}) + if len(subnetParams) >= 2 { + vnetName, subnetName := subnetParams[0], subnetParams[1] + linkedScope := azureshared.ExtractScopeFromResourceID(*pe.Properties.Subnet.ID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkSubnet.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(vnetName, subnetName), + Scope: linkedScope, + }, + }) + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkVirtualNetwork.String(), + Method: sdp.QueryMethod_GET, + Query: vnetName, + Scope: linkedScope, + }, + }) + } + } + + // Link to NetworkInterfaces (read-only array of NICs created for this private endpoint) + if pe.Properties != nil && pe.Properties.NetworkInterfaces != nil { + for _, iface := range pe.Properties.NetworkInterfaces { + if iface != nil && iface.ID != nil { + nicName := azureshared.ExtractResourceName(*iface.ID) + if nicName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(*iface.ID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkNetworkInterface.String(), + Method: sdp.QueryMethod_GET, + Query: nicName, + Scope: linkedScope, + }, + }) + } + } + } + } + + // Link to ApplicationSecurityGroups + if pe.Properties != nil && pe.Properties.ApplicationSecurityGroups != nil { + for _, asg := range pe.Properties.ApplicationSecurityGroups { + if asg != nil && asg.ID != nil { + asgName := azureshared.ExtractResourceName(*asg.ID) + if asgName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(*asg.ID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkApplicationSecurityGroup.String(), + Method: sdp.QueryMethod_GET, + Query: asgName, + Scope: linkedScope, + }, + }) + } + } + } + } + + // Link IPConfigurations[].Properties.PrivateIPAddress to stdlib ip (GET, global) + if pe.Properties != nil && pe.Properties.IPConfigurations != nil { + for _, ipConfig := range pe.Properties.IPConfigurations { + if ipConfig == nil || ipConfig.Properties == nil || ipConfig.Properties.PrivateIPAddress == nil { + continue + } + if *ipConfig.Properties.PrivateIPAddress != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *ipConfig.Properties.PrivateIPAddress, + Scope: "global", + }, + }) + } + } + } + + // Link to Private Link Services from PrivateLinkServiceConnections and ManualPrivateLinkServiceConnections + if pe.Properties != nil { + seenPLS := make(map[string]struct{}) + for _, conns := range [][]*armnetwork.PrivateLinkServiceConnection{ + pe.Properties.PrivateLinkServiceConnections, + pe.Properties.ManualPrivateLinkServiceConnections, + } { + for _, conn := range conns { + if conn == nil || conn.Properties == nil || conn.Properties.PrivateLinkServiceID == nil { + continue + } + plsID := *conn.Properties.PrivateLinkServiceID + if plsID == "" { + continue + } + if _, ok := seenPLS[plsID]; ok { + continue + } + seenPLS[plsID] = struct{}{} + plsName := azureshared.ExtractResourceName(plsID) + if plsName == "" { + continue + } + linkedScope := azureshared.ExtractScopeFromResourceID(plsID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPrivateLinkService.String(), + Method: sdp.QueryMethod_GET, + Query: plsName, + Scope: linkedScope, + }, + }) + } + } + } + + // Link CustomDnsConfigs: Fqdn -> stdlib dns (SEARCH, global), IPAddresses -> stdlib ip (GET, global) + if pe.Properties != nil && pe.Properties.CustomDNSConfigs != nil { + for _, dnsConfig := range pe.Properties.CustomDNSConfigs { + if dnsConfig == nil { + continue + } + if dnsConfig.Fqdn != nil && *dnsConfig.Fqdn != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: *dnsConfig.Fqdn, + Scope: "global", + }, + }) + } + if dnsConfig.IPAddresses != nil { + for _, ip := range dnsConfig.IPAddresses { + if ip != nil && *ip != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *ip, + Scope: "global", + }, + }) + } + } + } + } + } + + return sdpItem, nil +} + +func (n networkPrivateEndpointWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + NetworkPrivateEndpointLookupByName, + } +} + +func (n networkPrivateEndpointWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.NetworkSubnet, + azureshared.NetworkVirtualNetwork, + azureshared.NetworkNetworkInterface, + azureshared.NetworkApplicationSecurityGroup, + azureshared.NetworkPrivateLinkService, + stdlib.NetworkIP, + stdlib.NetworkDNS, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint +func (n networkPrivateEndpointWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_private_endpoint.name", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions-reference#microsoftnetwork +func (n networkPrivateEndpointWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Network/privateEndpoints/read", + } +} + +func (n networkPrivateEndpointWrapper) PredefinedRole() string { + return "Network Contributor" +} diff --git a/sources/azure/manual/network-private-endpoint_test.go b/sources/azure/manual/network-private-endpoint_test.go new file mode 100644 index 00000000..91f33dc2 --- /dev/null +++ b/sources/azure/manual/network-private-endpoint_test.go @@ -0,0 +1,334 @@ +package manual_test + +import ( + "context" + "errors" + "fmt" + "reflect" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "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/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 TestNetworkPrivateEndpoint(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) { + peName := "test-pe" + pe := createAzurePrivateEndpoint(peName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockPrivateEndpointsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, peName).Return( + armnetwork.PrivateEndpointsClientGetResponse{ + PrivateEndpoint: *pe, + }, nil) + + wrapper := manual.NewNetworkPrivateEndpoint(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], peName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.NetworkPrivateEndpoint.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkPrivateEndpoint, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != peName { + t.Errorf("Expected unique attribute value %s, got %s", peName, 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.NetworkSubnet.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), + ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), + }, { + ExpectedType: azureshared.NetworkVirtualNetwork.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-vnet", + ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), + }, { + ExpectedType: azureshared.NetworkNetworkInterface.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-nic", + ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), + }, { + ExpectedType: azureshared.NetworkApplicationSecurityGroup.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-asg", + ExpectedScope: fmt.Sprintf("%s.%s", subscriptionID, resourceGroup), + }, { + ExpectedType: stdlib.NetworkIP.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "10.0.0.10", + ExpectedScope: "global", + }, { + ExpectedType: stdlib.NetworkDNS.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "myendpoint.example.com", + ExpectedScope: "global", + }, { + ExpectedType: stdlib.NetworkIP.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "10.0.0.5", + ExpectedScope: "global", + }, + } + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_EmptyName", func(t *testing.T) { + mockClient := mocks.NewMockPrivateEndpointsClient(ctrl) + + wrapper := manual.NewNetworkPrivateEndpoint(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 private endpoint with empty name, but got nil") + } + }) + + t.Run("List", func(t *testing.T) { + pe1 := createAzurePrivateEndpoint("test-pe-1", subscriptionID, resourceGroup) + pe2 := createAzurePrivateEndpoint("test-pe-2", subscriptionID, resourceGroup) + + mockClient := mocks.NewMockPrivateEndpointsClient(ctrl) + mockPager := NewMockPrivateEndpointsPager(ctrl) + + gomock.InOrder( + mockPager.EXPECT().More().Return(true), + mockPager.EXPECT().NextPage(ctx).Return( + armnetwork.PrivateEndpointsClientListResponse{ + PrivateEndpointListResult: armnetwork.PrivateEndpointListResult{ + Value: []*armnetwork.PrivateEndpoint{pe1, pe2}, + }, + }, nil), + mockPager.EXPECT().More().Return(false), + ) + + mockClient.EXPECT().List(resourceGroup).Return(mockPager) + + wrapper := manual.NewNetworkPrivateEndpoint(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.GetType() != azureshared.NetworkPrivateEndpoint.String() { + t.Fatalf("Expected type %s, got: %s", azureshared.NetworkPrivateEndpoint, item.GetType()) + } + } + }) + + t.Run("List_WithNilName", func(t *testing.T) { + pe1 := createAzurePrivateEndpoint("test-pe-1", subscriptionID, resourceGroup) + pe2 := &armnetwork.PrivateEndpoint{ + Name: nil, + Location: to.Ptr("eastus"), + Tags: map[string]*string{"env": to.Ptr("test")}, + Properties: &armnetwork.PrivateEndpointProperties{ + ProvisioningState: to.Ptr(armnetwork.ProvisioningStateSucceeded), + }, + } + + mockClient := mocks.NewMockPrivateEndpointsClient(ctrl) + mockPager := NewMockPrivateEndpointsPager(ctrl) + + gomock.InOrder( + mockPager.EXPECT().More().Return(true), + mockPager.EXPECT().NextPage(ctx).Return( + armnetwork.PrivateEndpointsClientListResponse{ + PrivateEndpointListResult: armnetwork.PrivateEndpointListResult{ + Value: []*armnetwork.PrivateEndpoint{pe1, pe2}, + }, + }, nil), + mockPager.EXPECT().More().Return(false), + ) + + mockClient.EXPECT().List(resourceGroup).Return(mockPager) + + wrapper := manual.NewNetworkPrivateEndpoint(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)) + } + if sdpItems[0].UniqueAttributeValue() != "test-pe-1" { + t.Errorf("Expected item name 'test-pe-1', got: %s", sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("ErrorHandling", func(t *testing.T) { + expectedErr := errors.New("private endpoint not found") + + mockClient := mocks.NewMockPrivateEndpointsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent-pe").Return( + armnetwork.PrivateEndpointsClientGetResponse{}, expectedErr) + + wrapper := manual.NewNetworkPrivateEndpoint(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "nonexistent-pe", true) + if qErr == nil { + t.Fatal("Expected error when getting nonexistent private endpoint, got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := mocks.NewMockPrivateEndpointsClient(ctrl) + wrapper := manual.NewNetworkPrivateEndpoint(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + w := wrapper.(sources.Wrapper) + potentialLinks := w.PotentialLinks() + if len(potentialLinks) == 0 { + t.Error("Expected PotentialLinks to return at least one link type") + } + if !potentialLinks[azureshared.NetworkSubnet] { + t.Error("Expected PotentialLinks to include NetworkSubnet") + } + if !potentialLinks[azureshared.NetworkVirtualNetwork] { + t.Error("Expected PotentialLinks to include NetworkVirtualNetwork") + } + }) +} + +// MockPrivateEndpointsPager is a mock for PrivateEndpointsPager +type MockPrivateEndpointsPager struct { + ctrl *gomock.Controller + recorder *MockPrivateEndpointsPagerMockRecorder +} + +type MockPrivateEndpointsPagerMockRecorder struct { + mock *MockPrivateEndpointsPager +} + +func NewMockPrivateEndpointsPager(ctrl *gomock.Controller) *MockPrivateEndpointsPager { + mock := &MockPrivateEndpointsPager{ctrl: ctrl} + mock.recorder = &MockPrivateEndpointsPagerMockRecorder{mock} + return mock +} + +func (m *MockPrivateEndpointsPager) EXPECT() *MockPrivateEndpointsPagerMockRecorder { + return m.recorder +} + +func (m *MockPrivateEndpointsPager) More() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "More") + ret0, _ := ret[0].(bool) + return ret0 +} + +func (mr *MockPrivateEndpointsPagerMockRecorder) More() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeOf((*MockPrivateEndpointsPager)(nil).More)) +} + +func (m *MockPrivateEndpointsPager) NextPage(ctx context.Context) (armnetwork.PrivateEndpointsClientListResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NextPage", ctx) + ret0, _ := ret[0].(armnetwork.PrivateEndpointsClientListResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +func (mr *MockPrivateEndpointsPagerMockRecorder) NextPage(ctx interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeOf((*MockPrivateEndpointsPager)(nil).NextPage), ctx) +} + +func createAzurePrivateEndpoint(peName, subscriptionID, resourceGroup string) *armnetwork.PrivateEndpoint { + subnetID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet", subscriptionID, resourceGroup) + nicID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/networkInterfaces/test-nic", subscriptionID, resourceGroup) + asgID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/applicationSecurityGroups/test-asg", subscriptionID, resourceGroup) + + return &armnetwork.PrivateEndpoint{ + Name: to.Ptr(peName), + Location: to.Ptr("eastus"), + Tags: map[string]*string{ + "env": to.Ptr("test"), + "project": to.Ptr("testing"), + }, + Properties: &armnetwork.PrivateEndpointProperties{ + ProvisioningState: to.Ptr(armnetwork.ProvisioningStateSucceeded), + Subnet: &armnetwork.Subnet{ + ID: to.Ptr(subnetID), + }, + NetworkInterfaces: []*armnetwork.Interface{ + {ID: to.Ptr(nicID)}, + }, + ApplicationSecurityGroups: []*armnetwork.ApplicationSecurityGroup{ + {ID: to.Ptr(asgID)}, + }, + IPConfigurations: []*armnetwork.PrivateEndpointIPConfiguration{ + { + Properties: &armnetwork.PrivateEndpointIPConfigurationProperties{ + PrivateIPAddress: to.Ptr("10.0.0.10"), + }, + }, + }, + CustomDNSConfigs: []*armnetwork.CustomDNSConfigPropertiesFormat{ + { + Fqdn: to.Ptr("myendpoint.example.com"), + IPAddresses: []*string{to.Ptr("10.0.0.5")}, + }, + }, + }, + } +} diff --git a/sources/azure/manual/network-public-ip-address.go b/sources/azure/manual/network-public-ip-address.go index eecf5e3e..1206a087 100644 --- a/sources/azure/manual/network-public-ip-address.go +++ b/sources/azure/manual/network-public-ip-address.go @@ -5,7 +5,7 @@ import ( "errors" "strings" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/go/sdpcache" diff --git a/sources/azure/manual/network-public-ip-address_test.go b/sources/azure/manual/network-public-ip-address_test.go index f8971218..9fb6ee7b 100644 --- a/sources/azure/manual/network-public-ip-address_test.go +++ b/sources/azure/manual/network-public-ip-address_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/go/discovery" diff --git a/sources/azure/manual/network-route-table.go b/sources/azure/manual/network-route-table.go index e0c1e839..5c6cedc5 100644 --- a/sources/azure/manual/network-route-table.go +++ b/sources/azure/manual/network-route-table.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/sources" "github.com/overmindtech/cli/sources/azure/clients" diff --git a/sources/azure/manual/network-route-table_test.go b/sources/azure/manual/network-route-table_test.go index 56dbdb73..abd5eca2 100644 --- a/sources/azure/manual/network-route-table_test.go +++ b/sources/azure/manual/network-route-table_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/go/discovery" diff --git a/sources/azure/manual/network-subnet.go b/sources/azure/manual/network-subnet.go index 0cf0875d..031a9134 100644 --- a/sources/azure/manual/network-subnet.go +++ b/sources/azure/manual/network-subnet.go @@ -5,7 +5,7 @@ import ( "errors" "strings" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/go/sdpcache" diff --git a/sources/azure/manual/network-subnet_test.go b/sources/azure/manual/network-subnet_test.go index 0e2399cb..d6966a04 100644 --- a/sources/azure/manual/network-subnet_test.go +++ b/sources/azure/manual/network-subnet_test.go @@ -6,7 +6,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/go/discovery" diff --git a/sources/azure/manual/network-virtual-network.go b/sources/azure/manual/network-virtual-network.go index 1d24fe8b..354f93f9 100644 --- a/sources/azure/manual/network-virtual-network.go +++ b/sources/azure/manual/network-virtual-network.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/go/sdpcache" diff --git a/sources/azure/manual/network-virtual-network_test.go b/sources/azure/manual/network-virtual-network_test.go index 880b3c33..4b0c1c43 100644 --- a/sources/azure/manual/network-virtual-network_test.go +++ b/sources/azure/manual/network-virtual-network_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/go/discovery" diff --git a/sources/azure/shared/mocks/mock_application_gateways_client.go b/sources/azure/shared/mocks/mock_application_gateways_client.go index 88a547c5..2962bef8 100644 --- a/sources/azure/shared/mocks/mock_application_gateways_client.go +++ b/sources/azure/shared/mocks/mock_application_gateways_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_load_balancers_client.go b/sources/azure/shared/mocks/mock_load_balancers_client.go index a140c027..f8ef7a0b 100644 --- a/sources/azure/shared/mocks/mock_load_balancers_client.go +++ b/sources/azure/shared/mocks/mock_load_balancers_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_network_interfaces_client.go b/sources/azure/shared/mocks/mock_network_interfaces_client.go index 6547f1c9..29b5bd06 100644 --- a/sources/azure/shared/mocks/mock_network_interfaces_client.go +++ b/sources/azure/shared/mocks/mock_network_interfaces_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_network_private_endpoint_client.go b/sources/azure/shared/mocks/mock_network_private_endpoint_client.go new file mode 100644 index 00000000..d933e981 --- /dev/null +++ b/sources/azure/shared/mocks/mock_network_private_endpoint_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: network-private-endpoint-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_network_private_endpoint_client.go -package=mocks -source=network-private-endpoint-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockPrivateEndpointsClient is a mock of PrivateEndpointsClient interface. +type MockPrivateEndpointsClient struct { + ctrl *gomock.Controller + recorder *MockPrivateEndpointsClientMockRecorder + isgomock struct{} +} + +// MockPrivateEndpointsClientMockRecorder is the mock recorder for MockPrivateEndpointsClient. +type MockPrivateEndpointsClientMockRecorder struct { + mock *MockPrivateEndpointsClient +} + +// NewMockPrivateEndpointsClient creates a new mock instance. +func NewMockPrivateEndpointsClient(ctrl *gomock.Controller) *MockPrivateEndpointsClient { + mock := &MockPrivateEndpointsClient{ctrl: ctrl} + mock.recorder = &MockPrivateEndpointsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPrivateEndpointsClient) EXPECT() *MockPrivateEndpointsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockPrivateEndpointsClient) Get(ctx context.Context, resourceGroupName, privateEndpointName string) (armnetwork.PrivateEndpointsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, privateEndpointName) + ret0, _ := ret[0].(armnetwork.PrivateEndpointsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockPrivateEndpointsClientMockRecorder) Get(ctx, resourceGroupName, privateEndpointName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPrivateEndpointsClient)(nil).Get), ctx, resourceGroupName, privateEndpointName) +} + +// List mocks base method. +func (m *MockPrivateEndpointsClient) List(resourceGroupName string) clients.PrivateEndpointsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", resourceGroupName) + ret0, _ := ret[0].(clients.PrivateEndpointsPager) + return ret0 +} + +// List indicates an expected call of List. +func (mr *MockPrivateEndpointsClientMockRecorder) List(resourceGroupName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockPrivateEndpointsClient)(nil).List), resourceGroupName) +} diff --git a/sources/azure/shared/mocks/mock_network_security_groups_client.go b/sources/azure/shared/mocks/mock_network_security_groups_client.go index 734d3a13..7a61fc33 100644 --- a/sources/azure/shared/mocks/mock_network_security_groups_client.go +++ b/sources/azure/shared/mocks/mock_network_security_groups_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_public_ip_addresses_client.go b/sources/azure/shared/mocks/mock_public_ip_addresses_client.go index 29297475..7e382e67 100644 --- a/sources/azure/shared/mocks/mock_public_ip_addresses_client.go +++ b/sources/azure/shared/mocks/mock_public_ip_addresses_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_route_tables_client.go b/sources/azure/shared/mocks/mock_route_tables_client.go index a37bf0c0..954fd65b 100644 --- a/sources/azure/shared/mocks/mock_route_tables_client.go +++ b/sources/azure/shared/mocks/mock_route_tables_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_subnets_client.go b/sources/azure/shared/mocks/mock_subnets_client.go index 0d6532e3..91455aad 100644 --- a/sources/azure/shared/mocks/mock_subnets_client.go +++ b/sources/azure/shared/mocks/mock_subnets_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_virtual_networks_client.go b/sources/azure/shared/mocks/mock_virtual_networks_client.go index d979f6f1..85a33e04 100644 --- a/sources/azure/shared/mocks/mock_virtual_networks_client.go +++ b/sources/azure/shared/mocks/mock_virtual_networks_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v8" + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) From b4a1bb073359ee6691af4b3b2ec2cbdef447ccb7 Mon Sep 17 00:00:00 2001 From: jameslaneovermind <122231433+jameslaneovermind@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:07:29 +0000 Subject: [PATCH 12/74] [ENG-2781] Walk up directory tree to discover knowledge files (#4009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - The CLI now walks up parent directories to find `.overmind/knowledge/` when it's not in the current working directory, stopping at the `.git` boundary to avoid escaping the repository - Fixes the common monorepo/CI pattern where the CLI runs from a subdirectory (e.g. `environments/prod/`) but knowledge files live at the repo root - Adds debug-level logging showing which directory was resolved, so users can verify the behavior with `--log debug` ## Linear Ticket - **Ticket**: [ENG-2781](https://linear.app/overmind/issue/ENG-2781/implement-walk-up-directory-discovery-for-knowledge-files) — Implement walk-up directory discovery for knowledge files - **Purpose**: When running the CLI from a subdirectory, knowledge files at the repo root are silently ignored. This change walks up the directory tree to find them. - **Project**: Tribal Knowledge ## Changes - **`cli/knowledge/discover.go`** — New `FindKnowledgeDir` function that walks up from a start directory checking for `.overmind/knowledge/`, stopping at `.git` boundary or filesystem root. Also adds debug-level logging of the resolved path in `DiscoverAndConvert`. - **`cli/cmd/terraform_plan.go`** — Uses `FindKnowledgeDir(".")` instead of hardcoded `".overmind/knowledge/"` - **`cli/cmd/changes_submit_plan.go`** — Same call site update - **`cli/knowledge/discover_test.go`** — 7 new test cases: CWD, parent, grandparent, `.git` boundary stop, CWD priority, not found, `.git` + knowledge at same level - **`docs.overmind.tech/docs/knowledge/knowledge.md`** — Added discovery rule explaining walk-up behavior ## Deviations from Approved Plan Implementation matches the approved plan — no material deviations. All five parts (FindKnowledgeDir function, call site updates, debug logging, 7 unit tests, docs update) were implemented exactly as specified in the plan approved by Dylan Ratcliffe on ENG-2781. Closes ENG-2781 Made with [Cursor](https://cursor.com) --- > [!NOTE] > **Low Risk** > Small, well-tested change to local file discovery paths; primary risk is behavior differences in edge cases (multiple knowledge dirs, missing `.git`) affecting which knowledge files are uploaded. > > **Overview** > The CLI’s knowledge-file discovery now *walks up parent directories* to find `.overmind/knowledge/` (stopping at the `.git` boundary) instead of only looking in the current working directory. > > `overmind terraform plan` and `overmind changes submit-plan` now use this resolved directory when attaching knowledge to `StartChangeAnalysis`, `DiscoverAndConvert` logs the resolved path at debug level, and new unit tests + docs cover the new discovery behavior. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9d98bc3741f75e2bb874d790df1838da255dd0cf. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). Co-authored-by: Cursor GitOrigin-RevId: c87f8b056fa35558f608f54b7ec122967dfed9f8 --- cmd/changes_submit_plan.go | 3 +- cmd/terraform_plan.go | 3 +- knowledge/discover.go | 30 ++++++++ knowledge/discover_test.go | 138 +++++++++++++++++++++++++++++++++++++ 4 files changed, 172 insertions(+), 2 deletions(-) diff --git a/cmd/changes_submit_plan.go b/cmd/changes_submit_plan.go index e1eb5035..ab802f82 100644 --- a/cmd/changes_submit_plan.go +++ b/cmd/changes_submit_plan.go @@ -289,7 +289,8 @@ func SubmitPlan(cmd *cobra.Command, args []string) error { } // Discover and convert knowledge files - sdpKnowledge := knowledge.DiscoverAndConvert(ctx, ".overmind/knowledge/") + knowledgeDir := knowledge.FindKnowledgeDir(".") + sdpKnowledge := knowledge.DiscoverAndConvert(ctx, knowledgeDir) _, err = client.StartChangeAnalysis(ctx, &connect.Request[sdp.StartChangeAnalysisRequest]{ Msg: &sdp.StartChangeAnalysisRequest{ diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index 8a3d343e..045a1c33 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -316,7 +316,8 @@ func TerraformPlanImpl(ctx context.Context, cmd *cobra.Command, oi sdp.OvermindI log.WithField("change", changeUuid).Debug("Uploading planned changes") // Discover and convert knowledge files - sdpKnowledge := knowledge.DiscoverAndConvert(ctx, ".overmind/knowledge/") + knowledgeDir := knowledge.FindKnowledgeDir(".") + sdpKnowledge := knowledge.DiscoverAndConvert(ctx, knowledgeDir) _, err = client.StartChangeAnalysis(ctx, &connect.Request[sdp.StartChangeAnalysisRequest]{ Msg: &sdp.StartChangeAnalysisRequest{ diff --git a/knowledge/discover.go b/knowledge/discover.go index 5e92dbf6..f675d71c 100644 --- a/knowledge/discover.go +++ b/knowledge/discover.go @@ -45,6 +45,32 @@ const ( maxFileSize = 10 * 1024 * 1024 // 10MB ) +// FindKnowledgeDir walks up from startDir looking for a .overmind/knowledge/ +// directory. Returns the absolute path if found, or empty string if not. +// Stops at the repository root (.git boundary) or filesystem root to avoid +// picking up knowledge files from unrelated parent projects. +func FindKnowledgeDir(startDir string) string { + dir, err := filepath.Abs(startDir) + if err != nil { + return "" + } + for { + candidate := filepath.Join(dir, ".overmind", "knowledge") + if info, err := os.Stat(candidate); err == nil && info.IsDir() { + return candidate + } + if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil { + break + } + parent := filepath.Dir(dir) + if parent == dir { + break + } + dir = parent + } + return "" +} + // 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) { @@ -316,6 +342,10 @@ func validateDescription(description string) error { // 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 { + if knowledgeDir != "" { + log.WithContext(ctx).WithField("knowledgeDir", knowledgeDir).Debug("Resolved knowledge directory") + } + knowledgeFiles, warnings := Discover(knowledgeDir) // Log warnings diff --git a/knowledge/discover_test.go b/knowledge/discover_test.go index 2e8d21b0..1c953e04 100644 --- a/knowledge/discover_test.go +++ b/knowledge/discover_test.go @@ -651,6 +651,144 @@ M } } +// FindKnowledgeDir tests + +func TestFindKnowledgeDir_InCWD(t *testing.T) { + root := t.TempDir() + knowledgeDir := filepath.Join(root, ".overmind", "knowledge") + if err := os.MkdirAll(knowledgeDir, 0755); err != nil { + t.Fatal(err) + } + + result := FindKnowledgeDir(root) + + if result != knowledgeDir { + t.Errorf("expected %q, got %q", knowledgeDir, result) + } +} + +func TestFindKnowledgeDir_InParent(t *testing.T) { + root := t.TempDir() + knowledgeDir := filepath.Join(root, ".overmind", "knowledge") + if err := os.MkdirAll(knowledgeDir, 0755); err != nil { + t.Fatal(err) + } + childDir := filepath.Join(root, "environments", "prod") + if err := os.MkdirAll(childDir, 0755); err != nil { + t.Fatal(err) + } + + result := FindKnowledgeDir(childDir) + + if result != knowledgeDir { + t.Errorf("expected %q, got %q", knowledgeDir, result) + } +} + +func TestFindKnowledgeDir_InGrandparent(t *testing.T) { + root := t.TempDir() + knowledgeDir := filepath.Join(root, ".overmind", "knowledge") + if err := os.MkdirAll(knowledgeDir, 0755); err != nil { + t.Fatal(err) + } + deepDir := filepath.Join(root, "a", "b", "c") + if err := os.MkdirAll(deepDir, 0755); err != nil { + t.Fatal(err) + } + + result := FindKnowledgeDir(deepDir) + + if result != knowledgeDir { + t.Errorf("expected %q, got %q", knowledgeDir, result) + } +} + +func TestFindKnowledgeDir_StopsAtGitBoundary(t *testing.T) { + root := t.TempDir() + // Knowledge above the git boundary -- should NOT be found + knowledgeDir := filepath.Join(root, ".overmind", "knowledge") + if err := os.MkdirAll(knowledgeDir, 0755); err != nil { + t.Fatal(err) + } + // Git repo is a subdirectory + repoDir := filepath.Join(root, "my-repo") + if err := os.MkdirAll(filepath.Join(repoDir, ".git"), 0755); err != nil { + t.Fatal(err) + } + workDir := filepath.Join(repoDir, "environments", "prod") + if err := os.MkdirAll(workDir, 0755); err != nil { + t.Fatal(err) + } + + result := FindKnowledgeDir(workDir) + + if result != "" { + t.Errorf("expected empty string (should not escape .git boundary), got %q", result) + } +} + +func TestFindKnowledgeDir_CWDTakesPriority(t *testing.T) { + root := t.TempDir() + // Knowledge at root + rootKnowledge := filepath.Join(root, ".overmind", "knowledge") + if err := os.MkdirAll(rootKnowledge, 0755); err != nil { + t.Fatal(err) + } + // Knowledge also in subdirectory + childDir := filepath.Join(root, "sub") + childKnowledge := filepath.Join(childDir, ".overmind", "knowledge") + if err := os.MkdirAll(childKnowledge, 0755); err != nil { + t.Fatal(err) + } + + result := FindKnowledgeDir(childDir) + + if result != childKnowledge { + t.Errorf("expected CWD knowledge %q to take priority, got %q", childKnowledge, result) + } +} + +func TestFindKnowledgeDir_NotFoundAnywhere(t *testing.T) { + root := t.TempDir() + workDir := filepath.Join(root, "some", "dir") + if err := os.MkdirAll(workDir, 0755); err != nil { + t.Fatal(err) + } + // Place .git at root to create a boundary + if err := os.MkdirAll(filepath.Join(root, ".git"), 0755); err != nil { + t.Fatal(err) + } + + result := FindKnowledgeDir(workDir) + + if result != "" { + t.Errorf("expected empty string, got %q", result) + } +} + +func TestFindKnowledgeDir_GitBoundaryWithKnowledge(t *testing.T) { + root := t.TempDir() + // .git and .overmind/knowledge at the same level + if err := os.MkdirAll(filepath.Join(root, ".git"), 0755); err != nil { + t.Fatal(err) + } + knowledgeDir := filepath.Join(root, ".overmind", "knowledge") + if err := os.MkdirAll(knowledgeDir, 0755); err != nil { + t.Fatal(err) + } + workDir := filepath.Join(root, "environments", "prod") + if err := os.MkdirAll(workDir, 0755); err != nil { + t.Fatal(err) + } + + result := FindKnowledgeDir(workDir) + + // Should find knowledge at repo root before the .git stop triggers + if result != knowledgeDir { + t.Errorf("expected %q, got %q", knowledgeDir, result) + } +} + // Helper functions func writeFile(t *testing.T, path, content string) { From 70ceae324977b2cee139ff0d76249b4df7e15664 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 26 Feb 2026 12:29:32 +0100 Subject: [PATCH 13/74] Go fix (#3996) This applies the new `go fix` from go 1.26 to our code, cleaning up a bunch of outdated coding patterns. This PR also contains an update to golangci-lint to avoid a incompatibility in static check with the new code. Check out the commit messages for details. > [!NOTE] > **Low Risk** > Primarily mechanical refactors and tooling bumps; functional behavior changes are minimal aside from the small scope-check helper simplification. > > **Overview** > **Modernizes Go code and test fixtures** by replacing `interface{}` with `any`, `map[string]interface{}` with `map[string]any`, and simplifying scope checks in AWS adapter helpers via `slices.Contains`. > > **Removes custom pointer helper functions** used in AWS adapter tests and updates a large set of test data builders to use `new(T)`-style pointer creation instead; related Cursor docs/templates are adjusted to match (including dropping the Azure SDK `to.Ptr` helper guidance). > > **Tooling updates**: bumps `golangci-lint` from `v2.9.0` to `v2.10.1` in both devcontainer and CI, and adds `ripgrep` to the devcontainer image. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit bf1f8d1c6c0d0f8644627b1eec431f3a9d63386c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: d04ca21bb8e1a331035f484b6b0f8076d6c271b5 --- .../adapterhelpers_get_list_adapter_v2.go | 9 +- ...adapterhelpers_get_list_adapter_v2_test.go | 2 +- .../adapterhelpers_get_list_source.go | 9 +- .../adapters/adapterhelpers_shared_tests.go | 28 -- aws-source/adapters/adapterhelpers_util.go | 2 +- .../adapters/apigateway-domain-name_test.go | 26 +- .../adapters/apigateway-resource_test.go | 38 +- .../adapters/apigateway-rest-api_test.go | 16 +- .../autoscaling-auto-scaling-group_test.go | 140 +++--- .../autoscaling-auto-scaling-policy_test.go | 96 ++-- .../adapters/cloudfront-cache-policy_test.go | 24 +- ...front-continuous-deployment-policy_test.go | 18 +- .../adapters/cloudfront-distribution_test.go | 224 ++++----- .../adapters/cloudfront-function_test.go | 12 +- .../adapters/cloudfront-key-group_test.go | 8 +- .../cloudfront-origin-access-control_test.go | 6 +- .../cloudfront-origin-request-policy_test.go | 14 +- .../cloudfront-realtime-log-config_test.go | 12 +- ...cloudfront-response-headers-policy_test.go | 54 +-- .../cloudfront-streaming-distribution_test.go | 46 +- aws-source/adapters/cloudfront_test.go | 4 +- aws-source/adapters/cloudwatch-alarm_test.go | 74 +-- .../adapters/cloudwatch-instance-metric.go | 2 +- .../adapters/directconnect-connection_test.go | 32 +- .../directconnect-customer-metadata_test.go | 4 +- ...nnect-gateway-association-proposal_test.go | 18 +- ...direct-connect-gateway-association_test.go | 18 +- ...-direct-connect-gateway-attachment_test.go | 18 +- ...rectconnect-direct-connect-gateway_test.go | 18 +- .../directconnect-hosted-connection_test.go | 32 +- .../directconnect-interconnect_test.go | 28 +- aws-source/adapters/directconnect-lag_test.go | 44 +- .../adapters/directconnect-location_test.go | 6 +- ...directconnect-router-configuration_test.go | 18 +- .../directconnect-virtual-gateway_test.go | 4 +- .../directconnect-virtual-interface_test.go | 14 +- aws-source/adapters/dynamodb-backup_test.go | 50 +- aws-source/adapters/dynamodb-table_test.go | 96 ++-- aws-source/adapters/ec2-address_test.go | 18 +- .../ec2-capacity-reservation-fleet_test.go | 30 +- .../adapters/ec2-capacity-reservation_test.go | 30 +- .../ec2-egress-only-internet-gateway_test.go | 4 +- ...2-iam-instance-profile-association_test.go | 10 +- aws-source/adapters/ec2-image_test.go | 36 +- .../ec2-instance-event-window_test.go | 10 +- .../adapters/ec2-instance-status_test.go | 6 +- aws-source/adapters/ec2-instance_test.go | 160 +++---- .../adapters/ec2-internet-gateway_test.go | 10 +- aws-source/adapters/ec2-key-pair_test.go | 10 +- .../ec2-launch-template-version_test.go | 38 +- .../adapters/ec2-launch-template_test.go | 12 +- aws-source/adapters/ec2-nat-gateway_test.go | 44 +- aws-source/adapters/ec2-network-acl_test.go | 42 +- .../ec2-network-interface-permission_test.go | 6 +- .../adapters/ec2-network-interface_test.go | 70 +-- .../adapters/ec2-placement-group_test.go | 8 +- .../adapters/ec2-reserved-instance_test.go | 18 +- aws-source/adapters/ec2-route-table_test.go | 44 +- .../adapters/ec2-security-group-rule_test.go | 40 +- .../adapters/ec2-security-group_test.go | 20 +- aws-source/adapters/ec2-snapshot_test.go | 28 +- aws-source/adapters/ec2-subnet_test.go | 38 +- ...it-gateway-route-table-association_test.go | 4 +- ...it-gateway-route-table-propagation_test.go | 4 +- .../ec2-transit-gateway-route-table_test.go | 10 +- .../adapters/ec2-transit-gateway-route.go | 18 +- .../ec2-transit-gateway-route_test.go | 6 +- aws-source/adapters/ec2-volume-status_test.go | 28 +- aws-source/adapters/ec2-volume_test.go | 26 +- aws-source/adapters/ec2-vpc-endpoint_test.go | 34 +- .../ec2-vpc-peering-connection_test.go | 32 +- aws-source/adapters/ec2-vpc_test.go | 30 +- .../adapters/ecs-capacity-provider_test.go | 44 +- aws-source/adapters/ecs-cluster_test.go | 40 +- aws-source/adapters/ecs-container-instance.go | 4 +- .../adapters/ecs-container-instance_test.go | 192 ++++---- aws-source/adapters/ecs-service.go | 4 +- aws-source/adapters/ecs-service_test.go | 112 ++--- aws-source/adapters/ecs-task-definition.go | 6 +- .../adapters/ecs-task-definition_test.go | 108 ++--- aws-source/adapters/ecs-task.go | 6 +- aws-source/adapters/ecs-task_test.go | 90 ++-- aws-source/adapters/efs-access-point_test.go | 28 +- aws-source/adapters/efs-backup-policy_test.go | 2 +- aws-source/adapters/efs-file-system_test.go | 32 +- aws-source/adapters/efs-mount-target_test.go | 18 +- .../efs-replication-configuration_test.go | 22 +- aws-source/adapters/eks-addon_test.go | 24 +- aws-source/adapters/eks-cluster_test.go | 50 +- .../adapters/eks-fargate-profile_test.go | 12 +- aws-source/adapters/eks-nodegroup_test.go | 42 +- .../adapters/elb-instance-health_test.go | 8 +- aws-source/adapters/elb-load-balancer_test.go | 64 +-- aws-source/adapters/elbv2-listener.go | 4 +- aws-source/adapters/elbv2-listener_test.go | 12 +- .../adapters/elbv2-load-balancer_test.go | 30 +- aws-source/adapters/elbv2-rule_test.go | 14 +- .../adapters/elbv2-target-group_test.go | 28 +- .../adapters/elbv2-target-health_test.go | 48 +- aws-source/adapters/elbv2_test.go | 64 +-- aws-source/adapters/iam-group_test.go | 10 +- .../adapters/iam-instance-profile_test.go | 32 +- aws-source/adapters/iam-policy.go | 2 +- aws-source/adapters/iam-policy_test.go | 102 ++-- aws-source/adapters/iam-role_test.go | 70 +-- aws-source/adapters/iam-user_test.go | 66 +-- .../adapters/integration/apigateway/create.go | 29 +- .../adapters/integration/apigateway/delete.go | 4 +- .../integration/ec2-transit-gateway/setup.go | 38 +- .../ec2-transit-gateway/teardown.go | 8 +- aws-source/adapters/integration/kms/create.go | 9 +- .../adapters/integration/kms/kms_test.go | 2 +- .../adapters/integration/ssm/main_test.go | 5 +- aws-source/adapters/kms-alias_test.go | 10 +- .../adapters/kms-custom-key-store_test.go | 20 +- aws-source/adapters/kms-grant.go | 6 +- aws-source/adapters/kms-grant_test.go | 28 +- aws-source/adapters/kms-key-policy_test.go | 6 +- aws-source/adapters/kms-key_test.go | 36 +- .../lambda-event-source-mapping_test.go | 76 ++- aws-source/adapters/lambda-function_test.go | 88 ++-- aws-source/adapters/lambda-layer-version.go | 4 +- .../adapters/lambda-layer-version_test.go | 22 +- aws-source/adapters/lambda-layer_test.go | 12 +- aws-source/adapters/lambda.go | 8 +- .../network-firewall-firewall-policy_test.go | 34 +- .../network-firewall-firewall_test.go | 38 +- .../network-firewall-rule-group_test.go | 50 +- ...ewall-tls-inspection-configuration_test.go | 42 +- .../networkmanager-connect-attachment_test.go | 6 +- ...rkmanager-connect-peer-association_test.go | 8 +- .../networkmanager-connect-peer_test.go | 22 +- .../networkmanager-connection_test.go | 24 +- ...networkmanager-core-network-policy_test.go | 4 +- .../networkmanager-core-network_test.go | 14 +- .../adapters/networkmanager-device_test.go | 16 +- .../networkmanager-global-network_test.go | 4 +- .../networkmanager-link-association_test.go | 6 +- .../adapters/networkmanager-link_test.go | 16 +- ...rkmanager-network-resource-relationship.go | 4 +- ...ager-network-resource-relationship_test.go | 58 +-- ...anager-site-to-site-vpn-attachment_test.go | 6 +- .../adapters/networkmanager-site_test.go | 8 +- ...t-gateway-connect-peer-association_test.go | 8 +- ...orkmanager-transit-gateway-peering_test.go | 8 +- ...nager-transit-gateway-registration_test.go | 8 +- ...sit-gateway-route-table-attachment_test.go | 18 +- .../networkmanager-vpc-attachment_test.go | 4 +- .../rds-db-cluster-parameter-group_test.go | 66 +-- aws-source/adapters/rds-db-cluster_test.go | 146 +++--- aws-source/adapters/rds-db-instance_test.go | 180 +++---- .../adapters/rds-db-parameter-group_test.go | 66 +-- .../adapters/rds-db-subnet-group_test.go | 18 +- aws-source/adapters/rds-option-group_test.go | 12 +- aws-source/adapters/rds.go | 4 +- .../adapters/route53-health-check_test.go | 36 +- .../adapters/route53-hosted-zone_test.go | 14 +- .../route53-resource-record-set_test.go | 42 +- aws-source/adapters/s3.go | 2 +- aws-source/adapters/s3_test.go | 126 ++--- .../adapters/sns-data-protection-policy.go | 4 +- .../sns-data-protection-policy_test.go | 4 +- aws-source/adapters/sns-endpoint_test.go | 2 +- .../adapters/sns-platform-application_test.go | 10 +- aws-source/adapters/sns-subscription_test.go | 16 +- aws-source/adapters/sns-topic_test.go | 8 +- aws-source/adapters/sqs-queue.go | 4 +- aws-source/adapters/sqs-queue_test.go | 2 +- aws-source/adapters/ssm-parameter.go | 6 +- cmd/changes_submit_plan.go | 2 +- cmd/pterm.go | 6 +- cmd/terraform_plan.go | 8 +- cmd/theme.go | 42 +- cmd/version_check.go | 2 +- go.mod | 2 +- go/auth/auth.go | 6 +- go/auth/middleware.go | 9 +- go/auth/middleware_test.go | 6 +- go/auth/nats.go | 5 +- go/discovery/cmd.go | 2 +- go/discovery/engine.go | 2 +- go/discovery/engine_initerror_test.go | 6 +- go/discovery/getfindmutex_test.go | 7 +- go/discovery/performance_test.go | 2 +- go/discovery/querytracker_test.go | 6 +- go/sdp-go/instance_detect.go | 2 +- go/sdp-go/items.go | 54 +-- go/sdp-go/items_test.go | 68 +-- go/sdp-go/link_extract.go | 4 +- go/sdp-go/link_extract_test.go | 12 +- go/sdp-go/progress_test.go | 9 +- go/sdp-go/proto_clone_test.go | 2 +- go/sdp-go/sdpws/client.go | 12 +- go/sdp-go/sdpws/client_test.go | 44 +- go/sdp-go/test_utils.go | 4 +- go/sdpcache/bolt_cache.go | 18 +- go/sdpcache/cache.go | 2 +- go/sdpcache/cache_stuck_test.go | 48 +- go/sdpcache/cache_test.go | 118 ++--- go/sdpcache/item_generator_test.go | 2 +- go/tracing/deferlog.go | 2 +- k8s-source/adapters/generic_source.go | 11 +- knowledge/discover.go | 25 +- .../authorization-role-assignment_test.go | 5 +- .../batch-batch-accounts_test.go | 11 +- .../compute-availability-set_test.go | 69 ++- ...compute-capacity-reservation-group_test.go | 7 +- .../compute-dedicated-host-group_test.go | 9 +- .../compute-disk-access_test.go | 7 +- .../compute-disk-encryption-set_test.go | 17 +- .../integration-tests/compute-disk_test.go | 13 +- .../integration-tests/compute-image_test.go | 15 +- .../compute-proximity-placement-group_test.go | 9 +- .../compute-snapshot_test.go | 11 +- .../compute-virtual-machine-extension_test.go | 77 ++- ...ompute-virtual-machine-run-command_test.go | 77 ++- .../compute-virtual-machine-scale-set_test.go | 101 ++-- .../compute-virtual-machine_test.go | 59 ++- .../dbforpostgresql-database_test.go | 27 +- .../documentdb-database-accounts_test.go | 19 +- .../azure/integration-tests/helpers_test.go | 7 +- .../integration-tests/keyvault-vault_test.go | 13 +- ...gedidentity-user-assigned-identity_test.go | 7 +- .../network-application-gateway_test.go | 19 +- .../network-load-balancer_test.go | 81 ++-- .../network-network-interface_test.go | 11 +- .../network-network-security-group_test.go | 25 +- .../network-public-ip-address_test.go | 37 +- .../network-route-table_test.go | 13 +- .../integration-tests/network-zone_test.go | 9 +- .../integration-tests/sql-database_test.go | 25 +- .../storage-blob-container_test.go | 15 +- .../storage-fileshare_test.go | 3 +- .../authorization-role-assignment_test.go | 27 +- .../azure/manual/batch-batch-accounts_test.go | 37 +- .../manual/compute-availability-set_test.go | 47 +- ...compute-capacity-reservation-group_test.go | 23 +- .../compute-dedicated-host-group_test.go | 29 +- .../azure/manual/compute-disk-access_test.go | 31 +- .../compute-disk-encryption-set_test.go | 33 +- sources/azure/manual/compute-disk_test.go | 95 ++-- ...ompute-gallery-application-version_test.go | 19 +- .../compute-gallery-application_test.go | 8 +- .../manual/compute-gallery-image_test.go | 31 +- sources/azure/manual/compute-gallery_test.go | 19 +- sources/azure/manual/compute-image_test.go | 63 ++- .../compute-proximity-placement-group_test.go | 45 +- .../compute-shared-gallery-image_test.go | 29 +- sources/azure/manual/compute-snapshot_test.go | 123 +++-- .../compute-virtual-machine-extension_test.go | 41 +- ...ompute-virtual-machine-run-command_test.go | 27 +- .../compute-virtual-machine-scale-set_test.go | 69 ++- .../manual/compute-virtual-machine_test.go | 27 +- .../manual/dbforpostgresql-database_test.go | 13 +- .../dbforpostgresql-flexible-server_test.go | 35 +- .../documentdb-database-accounts_test.go | 47 +- .../azure/manual/keyvault-managed-hsm_test.go | 59 ++- sources/azure/manual/keyvault-secret_test.go | 42 +- sources/azure/manual/keyvault-vault_test.go | 51 +- sources/azure/manual/links_helpers.go | 7 +- ...gedidentity-user-assigned-identity_test.go | 25 +- .../network-application-gateway_test.go | 71 ++- .../manual/network-load-balancer_test.go | 70 ++- .../manual/network-network-interface_test.go | 42 +- .../network-network-security-group_test.go | 68 ++- .../manual/network-public-ip-address_test.go | 68 ++- .../azure/manual/network-route-table_test.go | 78 ++- sources/azure/manual/network-subnet_test.go | 11 +- .../manual/network-virtual-network_test.go | 54 +-- sources/azure/manual/network-zone_test.go | 66 ++- sources/azure/manual/sql-database_test.go | 32 +- .../manual/sql-server-firewall-rule_test.go | 24 +- sources/azure/manual/sql-server_test.go | 46 +- sources/azure/manual/storage-account_test.go | 25 +- .../manual/storage-blob-container_test.go | 31 +- .../azure/manual/storage-fileshare_test.go | 19 +- sources/azure/manual/storage-queues_test.go | 11 +- sources/azure/manual/storage-table_test.go | 16 +- sources/azure/proc/proc.go | 4 +- sources/azure/shared/resource_id_item_type.go | 16 +- sources/azure/shared/utils_test.go | 25 +- sources/gcp/dynamic/adapter.go | 9 +- .../ai-platform-batch-prediction-job_test.go | 6 +- .../adapters/ai-platform-custom-job_test.go | 2 +- .../adapters/ai-platform-endpoint_test.go | 2 +- ...rm-model-deployment-monitoring-job_test.go | 2 +- .../adapters/ai-platform-model_test.go | 2 +- .../adapters/ai-platform-pipeline-job_test.go | 2 +- ...uery-data-transfer-transfer-config_test.go | 2 +- .../big-table-admin-app-profile_test.go | 2 +- .../adapters/big-table-admin-backup_test.go | 2 +- .../adapters/big-table-admin-cluster_test.go | 4 +- .../adapters/big-table-admin-instance_test.go | 2 +- .../adapters/big-table-admin-table_test.go | 2 +- .../cloud-billing-billing-info_test.go | 2 +- .../adapters/cloud-build-build_test.go | 2 +- .../cloud-resource-manager-project_test.go | 2 +- .../cloud-resource-manager-tag-key_test.go | 4 +- .../cloud-resource-manager-tag-value_test.go | 2 +- .../adapters/cloudfunctions-function_test.go | 4 +- .../compute-external-vpn-gateway_test.go | 2 +- .../dynamic/adapters/compute-firewall_test.go | 2 +- .../adapters/compute-global-address_test.go | 57 +-- .../compute-global-forwarding-rule_test.go | 108 ++--- .../compute-http-health-check_test.go | 12 +- .../compute-network-endpoint-group_test.go | 2 +- .../dynamic/adapters/compute-network_test.go | 2 +- .../dynamic/adapters/compute-project_test.go | 2 +- .../compute-public-delegated-prefix_test.go | 2 +- .../compute-region-commitment_test.go | 2 +- .../dynamic/adapters/compute-route_test.go | 2 +- .../dynamic/adapters/compute-router_test.go | 44 +- .../adapters/compute-ssl-certificate_test.go | 20 +- .../adapters/compute-ssl-policy_test.go | 2 +- .../adapters/compute-subnetwork_test.go | 2 +- .../compute-target-http-proxy_test.go | 2 +- .../compute-target-https-proxy_test.go | 2 +- .../adapters/compute-target-pool_test.go | 2 +- .../dynamic/adapters/compute-url-map_test.go | 2 +- .../adapters/compute-vpn-gateway_test.go | 2 +- .../adapters/compute-vpn-tunnel_test.go | 2 +- .../adapters/container-cluster_test.go | 2 +- .../adapters/container-node-pool_test.go | 2 +- .../adapters/dataform-repository_test.go | 2 +- .../adapters/dataplex-aspect-type_test.go | 6 +- .../adapters/dataplex-data-scan_test.go | 2 +- .../adapters/dataplex-entry-group_test.go | 2 +- .../dataproc-auto-scaling-policy_test.go | 2 +- .../dynamic/adapters/dataproc-cluster_test.go | 2 +- .../dynamic/adapters/dns-managed-zone_test.go | 2 +- .../essential-contacts-contact_test.go | 2 +- .../dynamic/adapters/file-instance_test.go | 2 +- sources/gcp/dynamic/adapters/iam-role_test.go | 2 +- .../dynamic/adapters/logging-bucket_test.go | 2 +- .../gcp/dynamic/adapters/logging-link_test.go | 2 +- .../adapters/logging-saved-query_test.go | 2 +- .../adapters/monitoring-alert-policy_test.go | 2 +- .../monitoring-custom-dashboard_test.go | 2 +- .../monitoring-notification-channel_test.go | 2 +- .../dynamic/adapters/orgpolicy-policy_test.go | 2 +- .../adapters/pubsub-subscription_test.go | 2 +- .../gcp/dynamic/adapters/pubsub-topic_test.go | 2 +- .../dynamic/adapters/redis-instance_test.go | 2 +- .../gcp/dynamic/adapters/run-revision_test.go | 2 +- .../gcp/dynamic/adapters/run-service_test.go | 2 +- .../adapters/secret-manager-secret_test.go | 2 +- ...management-security-center-service_test.go | 2 +- .../service-directory-endpoint_test.go | 2 +- .../adapters/service-usage-service_test.go | 2 +- .../dynamic/adapters/sql-admin-backup_test.go | 2 +- .../adapters/sql-admin-instance_test.go | 2 +- .../dynamic/adapters/storage-bucket_test.go | 2 +- .../storage-transfer-transfer-job_test.go | 2 +- sources/gcp/dynamic/shared.go | 20 +- sources/gcp/dynamic/shared_test.go | 20 +- .../integration-tests/compute-address_test.go | 7 +- .../compute-autoscaler_test.go | 41 +- .../integration-tests/compute-disk_test.go | 7 +- .../compute-forwarding-rule_test.go | 11 +- .../compute-healthcheck_test.go | 11 +- .../integration-tests/compute-image_test.go | 5 +- .../compute-instance-group-manager_test.go | 21 +- .../compute-instance-group_test.go | 3 +- .../compute-instance_test.go | 19 +- .../compute-instant-snapshot_test.go | 5 +- .../compute-machine-image_test.go | 5 +- .../compute-node-group_test.go | 17 +- .../compute-reservation_test.go | 7 +- .../compute-snapshot_test.go | 5 +- .../kms_vs_asset_inventory_test.go | 19 +- .../service-account-impersonation_test.go | 27 +- .../cloud-kms-crypto-key-version_test.go | 14 +- .../gcp/manual/cloud-kms-crypto-key_test.go | 14 +- sources/gcp/manual/cloud-kms-key-ring_test.go | 14 +- sources/gcp/manual/compute-address.go | 3 +- sources/gcp/manual/compute-address_test.go | 11 +- sources/gcp/manual/compute-autoscaler.go | 3 +- sources/gcp/manual/compute-autoscaler_test.go | 13 +- sources/gcp/manual/compute-backend-service.go | 10 +- .../manual/compute-backend-service_test.go | 21 +- sources/gcp/manual/compute-disk.go | 3 +- sources/gcp/manual/compute-disk_test.go | 445 +++++++++--------- sources/gcp/manual/compute-forwarding-rule.go | 3 +- .../manual/compute-forwarding-rule_test.go | 21 +- sources/gcp/manual/compute-healthcheck.go | 10 +- .../gcp/manual/compute-healthcheck_test.go | 61 ++- sources/gcp/manual/compute-image_test.go | 37 +- .../manual/compute-instance-group-manager.go | 3 +- .../compute-instance-group-manager_test.go | 45 +- sources/gcp/manual/compute-instance-group.go | 3 +- .../gcp/manual/compute-instance-group_test.go | 9 +- sources/gcp/manual/compute-instance.go | 3 +- sources/gcp/manual/compute-instance_test.go | 55 ++- .../gcp/manual/compute-instant-snapshot.go | 3 +- .../manual/compute-instant-snapshot_test.go | 11 +- .../gcp/manual/compute-machine-image_test.go | 43 +- sources/gcp/manual/compute-node-group.go | 6 +- sources/gcp/manual/compute-node-group_test.go | 7 +- sources/gcp/manual/compute-node-template.go | 3 +- .../gcp/manual/compute-node-template_test.go | 11 +- ...pute-region-instance-group-manager_test.go | 15 +- sources/gcp/manual/compute-reservation.go | 3 +- .../gcp/manual/compute-reservation_test.go | 11 +- .../manual/compute-security-policy_test.go | 7 +- sources/gcp/manual/compute-snapshot_test.go | 27 +- .../gcp/manual/storage-bucket-iam-policy.go | 42 +- sources/gcp/proc/proc.go | 6 +- sources/gcp/proc/proc_test.go | 24 +- sources/gcp/shared/base.go | 19 +- sources/gcp/shared/kms-asset-loader.go | 4 +- sources/gcp/shared/location_info.go | 7 +- sources/gcp/shared/manual-adapter-links.go | 38 +- sources/shared/util.go | 2 +- sources/snapshot/adapters/index_test.go | 6 +- sources/snapshot/adapters/loader.go | 2 +- sources/snapshot/adapters/loader_test.go | 4 +- sources/transformer.go | 15 +- sources/transformer_test.go | 12 +- stdlib-source/adapters/certificate.go | 4 +- stdlib-source/adapters/certificate_test.go | 4 +- stdlib-source/adapters/dns.go | 8 +- stdlib-source/adapters/http.go | 8 +- stdlib-source/adapters/http_test.go | 12 +- stdlib-source/adapters/ip.go | 2 +- stdlib-source/adapters/main.go | 6 +- stdlib-source/adapters/rdap-asn.go | 2 +- stdlib-source/adapters/rdap-domain.go | 2 +- stdlib-source/adapters/rdap-entity.go | 2 +- stdlib-source/adapters/rdap-ip-network.go | 2 +- stdlib-source/adapters/rdap-nameserver.go | 2 +- tfutils/aws_config.go | 13 +- tfutils/azure_config.go | 2 +- tfutils/gcp_config.go | 2 +- tfutils/plan.go | 56 +-- tfutils/plan_mapper.go | 16 +- tfutils/plan_mapper_test.go | 102 ++-- 436 files changed, 5008 insertions(+), 5402 deletions(-) diff --git a/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go b/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go index 05099a01..d4c662b6 100644 --- a/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go +++ b/aws-source/adapters/adapterhelpers_get_list_adapter_v2.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "time" "github.com/overmindtech/cli/go/discovery" @@ -135,13 +136,7 @@ func (s *GetListAdapterV2[ListInput, ListOutput, AWSItem, ClientStruct, Options] return true } - for _, s := range s.Scopes() { - if s == scope { - return true - } - } - - return false + return slices.Contains(s.Scopes(), scope) } // Get retrieves an item from the adapter based on the provided scope, query, and 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 57f81e07..7d8b4f65 100644 --- a/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go +++ b/aws-source/adapters/adapterhelpers_get_list_adapter_v2_test.go @@ -261,7 +261,7 @@ func TestListFuncPaginatorBuilder(t *testing.T) { return output, nil }, ItemMapper: func(query *string, scope string, awsItem string) (*sdp.Item, error) { - attrs, _ := sdp.ToAttributes(map[string]interface{}{ + attrs, _ := sdp.ToAttributes(map[string]any{ "id": awsItem, }) return &sdp.Item{ diff --git a/aws-source/adapters/adapterhelpers_get_list_source.go b/aws-source/adapters/adapterhelpers_get_list_source.go index 35b5a739..2b50a44d 100644 --- a/aws-source/adapters/adapterhelpers_get_list_source.go +++ b/aws-source/adapters/adapterhelpers_get_list_source.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "time" "buf.build/go/protovalidate" @@ -108,13 +109,7 @@ func (s *GetListAdapter[AWSItem, ClientStruct, Options]) hasScope(scope string) return true } - for _, s := range s.Scopes() { - if s == scope { - return true - } - } - - return false + return slices.Contains(s.Scopes(), scope) } // Get retrieves an item from the adapter based on the provided scope, query, and diff --git a/aws-source/adapters/adapterhelpers_shared_tests.go b/aws-source/adapters/adapterhelpers_shared_tests.go index 8bf6cda0..da6bbd64 100644 --- a/aws-source/adapters/adapterhelpers_shared_tests.go +++ b/aws-source/adapters/adapterhelpers_shared_tests.go @@ -12,34 +12,6 @@ import ( "github.com/overmindtech/cli/go/sdp-go" ) -func PtrString(v string) *string { - return &v -} - -func PtrInt32(v int32) *int32 { - return &v -} - -func PtrInt64(v int64) *int64 { - return &v -} - -func PtrFloat32(v float32) *float32 { - return &v -} - -func PtrFloat64(v float64) *float64 { - return &v -} - -func PtrTime(v time.Time) *time.Time { - return &v -} - -func PtrBool(v bool) *bool { - return &v -} - type Subnet struct { ID *string CIDR string diff --git a/aws-source/adapters/adapterhelpers_util.go b/aws-source/adapters/adapterhelpers_util.go index c77ac4e4..1a2724b6 100644 --- a/aws-source/adapters/adapterhelpers_util.go +++ b/aws-source/adapters/adapterhelpers_util.go @@ -488,7 +488,7 @@ func GetAutoConfig(t *testing.T) (aws.Config, string, string) { // Converts an interface to SDP attributes using the `sdp.ToAttributesSorted` // function, and also allows the user to exclude certain top-level fields from // the resulting attributes -func ToAttributesWithExclude(i interface{}, exclusions ...string) (*sdp.ItemAttributes, error) { +func ToAttributesWithExclude(i any, exclusions ...string) (*sdp.ItemAttributes, error) { attrs, err := sdp.ToAttributesViaJson(i) if err != nil { return nil, err diff --git a/aws-source/adapters/apigateway-domain-name_test.go b/aws-source/adapters/apigateway-domain-name_test.go index 80798499..6e4243dc 100644 --- a/aws-source/adapters/apigateway-domain-name_test.go +++ b/aws-source/adapters/apigateway-domain-name_test.go @@ -43,21 +43,21 @@ import ( func TestDomainNameOutputMapper(t *testing.T) { domainName := &types.DomainName{ - CertificateArn: PtrString("arn:aws:acm:region:account-id:certificate/certificate-id"), - CertificateName: PtrString("certificate-name"), - CertificateUploadDate: PtrTime(time.Now()), - DistributionDomainName: PtrString("distribution-domain-name"), - DistributionHostedZoneId: PtrString("distribution-hosted-zone-id"), - DomainName: PtrString("domain-name"), + CertificateArn: new("arn:aws:acm:region:account-id:certificate/certificate-id"), + CertificateName: new("certificate-name"), + CertificateUploadDate: new(time.Now()), + DistributionDomainName: new("distribution-domain-name"), + DistributionHostedZoneId: new("distribution-hosted-zone-id"), + DomainName: new("domain-name"), DomainNameStatus: types.DomainNameStatusAvailable, - DomainNameStatusMessage: PtrString("status-message"), + DomainNameStatusMessage: new("status-message"), EndpointConfiguration: &types.EndpointConfiguration{Types: []types.EndpointType{types.EndpointTypeEdge}}, - MutualTlsAuthentication: &types.MutualTlsAuthentication{TruststoreUri: PtrString("truststore-uri")}, - OwnershipVerificationCertificateArn: PtrString("arn:aws:acm:region:account-id:certificate/ownership-verification-certificate-id"), - RegionalCertificateArn: PtrString("arn:aws:acm:region:account-id:certificate/regional-certificate-id"), - RegionalCertificateName: PtrString("regional-certificate-name"), - RegionalDomainName: PtrString("regional-domain-name"), - RegionalHostedZoneId: PtrString("regional-hosted-zone-id"), + MutualTlsAuthentication: &types.MutualTlsAuthentication{TruststoreUri: new("truststore-uri")}, + OwnershipVerificationCertificateArn: new("arn:aws:acm:region:account-id:certificate/ownership-verification-certificate-id"), + RegionalCertificateArn: new("arn:aws:acm:region:account-id:certificate/regional-certificate-id"), + RegionalCertificateName: new("regional-certificate-name"), + RegionalDomainName: new("regional-domain-name"), + RegionalHostedZoneId: new("regional-hosted-zone-id"), SecurityPolicy: types.SecurityPolicyTls12, Tags: map[string]string{"key": "value"}, } diff --git a/aws-source/adapters/apigateway-resource_test.go b/aws-source/adapters/apigateway-resource_test.go index 94352d27..93e18c8a 100644 --- a/aws-source/adapters/apigateway-resource_test.go +++ b/aws-source/adapters/apigateway-resource_test.go @@ -83,25 +83,25 @@ import ( func TestResourceOutputMapper(t *testing.T) { resource := &types.Resource{ - Id: PtrString("test-id"), - ParentId: PtrString("parent-id"), - Path: PtrString("/test-path"), - PathPart: PtrString("test-path-part"), + Id: new("test-id"), + ParentId: new("parent-id"), + Path: new("/test-path"), + PathPart: new("test-path-part"), ResourceMethods: map[string]types.Method{ "GET": { - ApiKeyRequired: PtrBool(true), + ApiKeyRequired: new(true), AuthorizationScopes: []string{"scope1", "scope2"}, - AuthorizationType: PtrString("NONE"), - AuthorizerId: PtrString("authorizer-id"), - HttpMethod: PtrString("GET"), + AuthorizationType: new("NONE"), + AuthorizerId: new("authorizer-id"), + HttpMethod: new("GET"), MethodIntegration: &types.Integration{ CacheKeyParameters: []string{"param1", "param2"}, - CacheNamespace: PtrString("namespace"), - ConnectionId: PtrString("connection-id"), + CacheNamespace: new("namespace"), + ConnectionId: new("connection-id"), ConnectionType: types.ConnectionTypeInternet, ContentHandling: types.ContentHandlingStrategyConvertToBinary, - Credentials: PtrString("credentials"), - HttpMethod: PtrString("POST"), + Credentials: new("credentials"), + HttpMethod: new("POST"), IntegrationResponses: map[string]types.IntegrationResponse{ "200": { ContentHandling: types.ContentHandlingStrategyConvertToText, @@ -111,11 +111,11 @@ func TestResourceOutputMapper(t *testing.T) { ResponseTemplates: map[string]string{ "template1": "value1", }, - SelectionPattern: PtrString("pattern"), - StatusCode: PtrString("200"), + SelectionPattern: new("pattern"), + StatusCode: new("200"), }, }, - PassthroughBehavior: PtrString("WHEN_NO_MATCH"), + PassthroughBehavior: new("WHEN_NO_MATCH"), RequestParameters: map[string]string{ "param1": "value1", }, @@ -127,7 +127,7 @@ func TestResourceOutputMapper(t *testing.T) { InsecureSkipVerification: false, }, Type: types.IntegrationTypeAwsProxy, - Uri: PtrString("uri"), + Uri: new("uri"), }, MethodResponses: map[string]types.MethodResponse{ "200": { @@ -137,17 +137,17 @@ func TestResourceOutputMapper(t *testing.T) { ResponseParameters: map[string]bool{ "param1": true, }, - StatusCode: PtrString("200"), + StatusCode: new("200"), }, }, - OperationName: PtrString("operation"), + OperationName: new("operation"), RequestModels: map[string]string{ "model1": "value1", }, RequestParameters: map[string]bool{ "param1": true, }, - RequestValidatorId: PtrString("validator-id"), + RequestValidatorId: new("validator-id"), }, }, } diff --git a/aws-source/adapters/apigateway-rest-api_test.go b/aws-source/adapters/apigateway-rest-api_test.go index 459fd95b..4f283380 100644 --- a/aws-source/adapters/apigateway-rest-api_test.go +++ b/aws-source/adapters/apigateway-rest-api_test.go @@ -39,22 +39,22 @@ func TestRestApiOutputMapper(t *testing.T) { output := &apigateway.GetRestApiOutput{ ApiKeySource: types.ApiKeySourceTypeHeader, BinaryMediaTypes: []string{"application/json"}, - CreatedDate: PtrTime(time.Now()), - Description: PtrString("Example API"), + CreatedDate: new(time.Now()), + Description: new("Example API"), DisableExecuteApiEndpoint: false, EndpointConfiguration: &types.EndpointConfiguration{ Types: []types.EndpointType{types.EndpointTypePrivate}, VpcEndpointIds: []string{"vpce-12345678"}, }, - Id: PtrString("abc123"), - MinimumCompressionSize: PtrInt32(1024), - Name: PtrString("ExampleAPI"), - Policy: PtrString("{\"Version\": \"2012-10-17\", \"Statement\": [{\"Effect\": \"Allow\", \"Principal\": \"*\", \"Action\": \"execute-api:Invoke\", \"Resource\": \"*\"}]}"), - RootResourceId: PtrString("root123"), + Id: new("abc123"), + MinimumCompressionSize: new(int32(1024)), + Name: new("ExampleAPI"), + Policy: new("{\"Version\": \"2012-10-17\", \"Statement\": [{\"Effect\": \"Allow\", \"Principal\": \"*\", \"Action\": \"execute-api:Invoke\", \"Resource\": \"*\"}]}"), + RootResourceId: new("root123"), Tags: map[string]string{ "env": "production", }, - Version: PtrString("v1"), + Version: new("v1"), Warnings: []string{"This is a warning"}, } diff --git a/aws-source/adapters/autoscaling-auto-scaling-group_test.go b/aws-source/adapters/autoscaling-auto-scaling-group_test.go index fb14825b..a17a8784 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-group_test.go +++ b/aws-source/adapters/autoscaling-auto-scaling-group_test.go @@ -17,33 +17,33 @@ func TestAutoScalingGroupOutputMapper(t *testing.T) { output := autoscaling.DescribeAutoScalingGroupsOutput{ AutoScalingGroups: []types.AutoScalingGroup{ { - AutoScalingGroupName: PtrString("eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), - AutoScalingGroupARN: PtrString("arn:aws:autoscaling:eu-west-2:944651592624:autoScalingGroup:1cbb0e22-818f-4d8b-8662-77f73d3713ca:autoScalingGroupName/eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), + AutoScalingGroupName: new("eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), + AutoScalingGroupARN: new("arn:aws:autoscaling:eu-west-2:944651592624:autoScalingGroup:1cbb0e22-818f-4d8b-8662-77f73d3713ca:autoScalingGroupName/eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), MixedInstancesPolicy: &types.MixedInstancesPolicy{ LaunchTemplate: &types.LaunchTemplate{ LaunchTemplateSpecification: &types.LaunchTemplateSpecification{ - LaunchTemplateId: PtrString("lt-0174ff2b8909d0c75"), // link - LaunchTemplateName: PtrString("eks-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), - Version: PtrString("1"), + LaunchTemplateId: new("lt-0174ff2b8909d0c75"), // link + LaunchTemplateName: new("eks-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), + Version: new("1"), }, Overrides: []types.LaunchTemplateOverrides{ { - InstanceType: PtrString("t3.large"), + InstanceType: new("t3.large"), }, }, }, InstancesDistribution: &types.InstancesDistribution{ - OnDemandAllocationStrategy: PtrString("prioritized"), - OnDemandBaseCapacity: PtrInt32(0), - OnDemandPercentageAboveBaseCapacity: PtrInt32(100), - SpotAllocationStrategy: PtrString("lowest-price"), - SpotInstancePools: PtrInt32(2), + OnDemandAllocationStrategy: new("prioritized"), + OnDemandBaseCapacity: new(int32(0)), + OnDemandPercentageAboveBaseCapacity: new(int32(100)), + SpotAllocationStrategy: new("lowest-price"), + SpotInstancePools: new(int32(2)), }, }, - MinSize: PtrInt32(1), - MaxSize: PtrInt32(3), - DesiredCapacity: PtrInt32(1), - DefaultCooldown: PtrInt32(300), + MinSize: new(int32(1)), + MaxSize: new(int32(3)), + DesiredCapacity: new(int32(1)), + DefaultCooldown: new(int32(300)), AvailabilityZones: []string{ // link "eu-west-2c", "eu-west-2a", @@ -53,62 +53,62 @@ func TestAutoScalingGroupOutputMapper(t *testing.T) { TargetGroupARNs: []string{ "arn:partition:service:region:account-id:resource-type/resource-id", // link }, - HealthCheckType: PtrString("EC2"), - HealthCheckGracePeriod: PtrInt32(15), + HealthCheckType: new("EC2"), + HealthCheckGracePeriod: new(int32(15)), Instances: []types.Instance{ { - InstanceId: PtrString("i-0be6c4fe789cb1b78"), // link - InstanceType: PtrString("t3.large"), - AvailabilityZone: PtrString("eu-west-2c"), + InstanceId: new("i-0be6c4fe789cb1b78"), // link + InstanceType: new("t3.large"), + AvailabilityZone: new("eu-west-2c"), LifecycleState: types.LifecycleStateInService, - HealthStatus: PtrString("Healthy"), + HealthStatus: new("Healthy"), LaunchTemplate: &types.LaunchTemplateSpecification{ - LaunchTemplateId: PtrString("lt-0174ff2b8909d0c75"), // Link - LaunchTemplateName: PtrString("eks-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), - Version: PtrString("1"), + LaunchTemplateId: new("lt-0174ff2b8909d0c75"), // Link + LaunchTemplateName: new("eks-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), + Version: new("1"), }, - ProtectedFromScaleIn: PtrBool(false), + ProtectedFromScaleIn: new(false), }, }, - CreatedTime: PtrTime(time.Now()), + CreatedTime: new(time.Now()), SuspendedProcesses: []types.SuspendedProcess{}, - VPCZoneIdentifier: PtrString("subnet-0e234bef35fc4a9e1,subnet-09d5f6fa75b0b4569,subnet-0960234bbc4edca03"), + VPCZoneIdentifier: new("subnet-0e234bef35fc4a9e1,subnet-09d5f6fa75b0b4569,subnet-0960234bbc4edca03"), EnabledMetrics: []types.EnabledMetric{}, Tags: []types.TagDescription{ { - ResourceId: PtrString("eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), - ResourceType: PtrString("auto-scaling-group"), - Key: PtrString("eks:cluster-name"), - Value: PtrString("dogfood"), - PropagateAtLaunch: PtrBool(true), + ResourceId: new("eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), + ResourceType: new("auto-scaling-group"), + Key: new("eks:cluster-name"), + Value: new("dogfood"), + PropagateAtLaunch: new(true), }, { - ResourceId: PtrString("eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), - ResourceType: PtrString("auto-scaling-group"), - Key: PtrString("eks:nodegroup-name"), - Value: PtrString("default-20230117110031319900000013"), - PropagateAtLaunch: PtrBool(true), + ResourceId: new("eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), + ResourceType: new("auto-scaling-group"), + Key: new("eks:nodegroup-name"), + Value: new("default-20230117110031319900000013"), + PropagateAtLaunch: new(true), }, { - ResourceId: PtrString("eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), - ResourceType: PtrString("auto-scaling-group"), - Key: PtrString("k8s.io/cluster-autoscaler/dogfood"), - Value: PtrString("owned"), - PropagateAtLaunch: PtrBool(true), + ResourceId: new("eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), + ResourceType: new("auto-scaling-group"), + Key: new("k8s.io/cluster-autoscaler/dogfood"), + Value: new("owned"), + PropagateAtLaunch: new(true), }, { - ResourceId: PtrString("eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), - ResourceType: PtrString("auto-scaling-group"), - Key: PtrString("k8s.io/cluster-autoscaler/enabled"), - Value: PtrString("true"), - PropagateAtLaunch: PtrBool(true), + ResourceId: new("eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), + ResourceType: new("auto-scaling-group"), + Key: new("k8s.io/cluster-autoscaler/enabled"), + Value: new("true"), + PropagateAtLaunch: new(true), }, { - ResourceId: PtrString("eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), - ResourceType: PtrString("auto-scaling-group"), - Key: PtrString("kubernetes.io/cluster/dogfood"), - Value: PtrString("owned"), - PropagateAtLaunch: PtrBool(true), + ResourceId: new("eks-default-20230117110031319900000013-96c2dfb1-a11b-b5e4-6efb-0fea7e22855c"), + ResourceType: new("auto-scaling-group"), + Key: new("kubernetes.io/cluster/dogfood"), + Value: new("owned"), + PropagateAtLaunch: new(true), }, }, TerminationPolicies: []string{ @@ -116,36 +116,36 @@ func TestAutoScalingGroupOutputMapper(t *testing.T) { "OldestLaunchTemplate", "OldestInstance", }, - NewInstancesProtectedFromScaleIn: PtrBool(false), - ServiceLinkedRoleARN: PtrString("arn:aws:iam::944651592624:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling"), // link - CapacityRebalance: PtrBool(true), + NewInstancesProtectedFromScaleIn: new(false), + ServiceLinkedRoleARN: new("arn:aws:iam::944651592624:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling"), // link + CapacityRebalance: new(true), TrafficSources: []types.TrafficSourceIdentifier{ { - Identifier: PtrString("arn:partition:service:region:account-id:resource-type/resource-id"), // We will skip this for now since it's related to VPC lattice groups which are still in preview + Identifier: new("arn:partition:service:region:account-id:resource-type/resource-id"), // We will skip this for now since it's related to VPC lattice groups which are still in preview }, }, - Context: PtrString("foo"), - DefaultInstanceWarmup: PtrInt32(10), - DesiredCapacityType: PtrString("foo"), - LaunchConfigurationName: PtrString("launchConfig"), // link + Context: new("foo"), + DefaultInstanceWarmup: new(int32(10)), + DesiredCapacityType: new("foo"), + LaunchConfigurationName: new("launchConfig"), // link LaunchTemplate: &types.LaunchTemplateSpecification{ - LaunchTemplateId: PtrString("id"), // link - LaunchTemplateName: PtrString("launchTemplateName"), + LaunchTemplateId: new("id"), // link + LaunchTemplateName: new("launchTemplateName"), }, - MaxInstanceLifetime: PtrInt32(30), - PlacementGroup: PtrString("placementGroup"), // link (ec2) - PredictedCapacity: PtrInt32(1), - Status: PtrString("OK"), + MaxInstanceLifetime: new(int32(30)), + PlacementGroup: new("placementGroup"), // link (ec2) + PredictedCapacity: new(int32(1)), + Status: new("OK"), WarmPoolConfiguration: &types.WarmPoolConfiguration{ InstanceReusePolicy: &types.InstanceReusePolicy{ - ReuseOnScaleIn: PtrBool(true), + ReuseOnScaleIn: new(true), }, - MaxGroupPreparedCapacity: PtrInt32(1), - MinSize: PtrInt32(1), + MaxGroupPreparedCapacity: new(int32(1)), + MinSize: new(int32(1)), PoolState: types.WarmPoolStateHibernated, Status: types.WarmPoolStatusPendingDelete, }, - WarmPoolSize: PtrInt32(1), + WarmPoolSize: new(int32(1)), }, }, } diff --git a/aws-source/adapters/autoscaling-auto-scaling-policy_test.go b/aws-source/adapters/autoscaling-auto-scaling-policy_test.go index 70929e68..ea1afe02 100644 --- a/aws-source/adapters/autoscaling-auto-scaling-policy_test.go +++ b/aws-source/adapters/autoscaling-auto-scaling-policy_test.go @@ -16,86 +16,86 @@ func TestScalingPolicyOutputMapper(t *testing.T) { output := autoscaling.DescribePoliciesOutput{ ScalingPolicies: []types.ScalingPolicy{ { - PolicyName: PtrString("scale-up-policy"), - PolicyARN: PtrString("arn:aws:autoscaling:us-east-1:123456789012:scalingPolicy:12345678-1234-1234-1234-123456789012:autoScalingGroupName/my-asg:policyName/scale-up-policy"), - AutoScalingGroupName: PtrString("my-asg"), - PolicyType: PtrString("TargetTrackingScaling"), - AdjustmentType: PtrString("ChangeInCapacity"), - MinAdjustmentMagnitude: PtrInt32(1), - ScalingAdjustment: PtrInt32(1), - Cooldown: PtrInt32(300), - MetricAggregationType: PtrString("Average"), - EstimatedInstanceWarmup: PtrInt32(300), - Enabled: PtrBool(true), + PolicyName: new("scale-up-policy"), + PolicyARN: new("arn:aws:autoscaling:us-east-1:123456789012:scalingPolicy:12345678-1234-1234-1234-123456789012:autoScalingGroupName/my-asg:policyName/scale-up-policy"), + AutoScalingGroupName: new("my-asg"), + PolicyType: new("TargetTrackingScaling"), + AdjustmentType: new("ChangeInCapacity"), + MinAdjustmentMagnitude: new(int32(1)), + ScalingAdjustment: new(int32(1)), + Cooldown: new(int32(300)), + MetricAggregationType: new("Average"), + EstimatedInstanceWarmup: new(int32(300)), + Enabled: new(true), TargetTrackingConfiguration: &types.TargetTrackingConfiguration{ PredefinedMetricSpecification: &types.PredefinedMetricSpecification{ PredefinedMetricType: types.MetricTypeALBRequestCountPerTarget, - ResourceLabel: PtrString("app/my-alb/778d41231b141a0f/targetgroup/my-alb-target-group/943f017f100becff"), + ResourceLabel: new("app/my-alb/778d41231b141a0f/targetgroup/my-alb-target-group/943f017f100becff"), }, - TargetValue: PtrFloat64(50.0), + TargetValue: new(50.0), }, Alarms: []types.Alarm{ { - AlarmName: PtrString("my-alarm-high"), - AlarmARN: PtrString("arn:aws:cloudwatch:us-east-1:123456789012:alarm:my-alarm-high"), + AlarmName: new("my-alarm-high"), + AlarmARN: new("arn:aws:cloudwatch:us-east-1:123456789012:alarm:my-alarm-high"), }, { - AlarmName: PtrString("my-alarm-low"), - AlarmARN: PtrString("arn:aws:cloudwatch:us-east-1:123456789012:alarm:my-alarm-low"), + AlarmName: new("my-alarm-low"), + AlarmARN: new("arn:aws:cloudwatch:us-east-1:123456789012:alarm:my-alarm-low"), }, }, }, { - PolicyName: PtrString("step-scaling-policy"), - PolicyARN: PtrString("arn:aws:autoscaling:us-east-1:123456789012:scalingPolicy:87654321-4321-4321-4321-210987654321:autoScalingGroupName/my-asg:policyName/step-scaling-policy"), - AutoScalingGroupName: PtrString("my-asg"), - PolicyType: PtrString("StepScaling"), - AdjustmentType: PtrString("PercentChangeInCapacity"), - MinAdjustmentMagnitude: PtrInt32(2), - MetricAggregationType: PtrString("Average"), - EstimatedInstanceWarmup: PtrInt32(60), - Enabled: PtrBool(true), + PolicyName: new("step-scaling-policy"), + PolicyARN: new("arn:aws:autoscaling:us-east-1:123456789012:scalingPolicy:87654321-4321-4321-4321-210987654321:autoScalingGroupName/my-asg:policyName/step-scaling-policy"), + AutoScalingGroupName: new("my-asg"), + PolicyType: new("StepScaling"), + AdjustmentType: new("PercentChangeInCapacity"), + MinAdjustmentMagnitude: new(int32(2)), + MetricAggregationType: new("Average"), + EstimatedInstanceWarmup: new(int32(60)), + Enabled: new(true), StepAdjustments: []types.StepAdjustment{ { - MetricIntervalLowerBound: PtrFloat64(0.0), - MetricIntervalUpperBound: PtrFloat64(10.0), - ScalingAdjustment: PtrInt32(10), + MetricIntervalLowerBound: new(0.0), + MetricIntervalUpperBound: new(10.0), + ScalingAdjustment: new(int32(10)), }, { - MetricIntervalLowerBound: PtrFloat64(10.0), - ScalingAdjustment: PtrInt32(20), + MetricIntervalLowerBound: new(10.0), + ScalingAdjustment: new(int32(20)), }, }, Alarms: []types.Alarm{ { - AlarmName: PtrString("step-alarm"), - AlarmARN: PtrString("arn:aws:cloudwatch:us-east-1:123456789012:alarm:step-alarm"), + AlarmName: new("step-alarm"), + AlarmARN: new("arn:aws:cloudwatch:us-east-1:123456789012:alarm:step-alarm"), }, }, }, { - PolicyName: PtrString("simple-scaling-policy"), - PolicyARN: PtrString("arn:aws:autoscaling:us-east-1:123456789012:scalingPolicy:11111111-2222-3333-4444-555555555555:autoScalingGroupName/another-asg:policyName/simple-scaling-policy"), - AutoScalingGroupName: PtrString("another-asg"), - PolicyType: PtrString("SimpleScaling"), - AdjustmentType: PtrString("ExactCapacity"), - ScalingAdjustment: PtrInt32(5), - Cooldown: PtrInt32(600), - Enabled: PtrBool(false), + PolicyName: new("simple-scaling-policy"), + PolicyARN: new("arn:aws:autoscaling:us-east-1:123456789012:scalingPolicy:11111111-2222-3333-4444-555555555555:autoScalingGroupName/another-asg:policyName/simple-scaling-policy"), + AutoScalingGroupName: new("another-asg"), + PolicyType: new("SimpleScaling"), + AdjustmentType: new("ExactCapacity"), + ScalingAdjustment: new(int32(5)), + Cooldown: new(int32(600)), + Enabled: new(false), }, { - PolicyName: PtrString("predictive-scaling-policy"), - PolicyARN: PtrString("arn:aws:autoscaling:us-east-1:123456789012:scalingPolicy:99999999-8888-7777-6666-555555555555:autoScalingGroupName/predictive-asg:policyName/predictive-scaling-policy"), - AutoScalingGroupName: PtrString("predictive-asg"), - PolicyType: PtrString("PredictiveScaling"), - Enabled: PtrBool(true), + PolicyName: new("predictive-scaling-policy"), + PolicyARN: new("arn:aws:autoscaling:us-east-1:123456789012:scalingPolicy:99999999-8888-7777-6666-555555555555:autoScalingGroupName/predictive-asg:policyName/predictive-scaling-policy"), + AutoScalingGroupName: new("predictive-asg"), + PolicyType: new("PredictiveScaling"), + Enabled: new(true), PredictiveScalingConfiguration: &types.PredictiveScalingConfiguration{ MetricSpecifications: []types.PredictiveScalingMetricSpecification{ { - TargetValue: PtrFloat64(40.0), + TargetValue: new(40.0), PredefinedMetricPairSpecification: &types.PredictiveScalingPredefinedMetricPair{ PredefinedMetricType: types.PredefinedMetricPairTypeALBRequestCount, - ResourceLabel: PtrString("app/predictive-alb/abc123def456/targetgroup/predictive-tg/789xyz"), + ResourceLabel: new("app/predictive-alb/abc123def456/targetgroup/predictive-tg/789xyz"), }, }, }, diff --git a/aws-source/adapters/cloudfront-cache-policy_test.go b/aws-source/adapters/cloudfront-cache-policy_test.go index 8380c496..e90c0d73 100644 --- a/aws-source/adapters/cloudfront-cache-policy_test.go +++ b/aws-source/adapters/cloudfront-cache-policy_test.go @@ -11,29 +11,29 @@ import ( ) var testCachePolicy = &types.CachePolicy{ - Id: PtrString("test-id"), - LastModifiedTime: PtrTime(time.Now()), + Id: new("test-id"), + LastModifiedTime: new(time.Now()), CachePolicyConfig: &types.CachePolicyConfig{ - MinTTL: PtrInt64(1), - Name: PtrString("test-name"), - Comment: PtrString("test-comment"), - DefaultTTL: PtrInt64(1), - MaxTTL: PtrInt64(1), + MinTTL: new(int64(1)), + Name: new("test-name"), + Comment: new("test-comment"), + DefaultTTL: new(int64(1)), + MaxTTL: new(int64(1)), ParametersInCacheKeyAndForwardedToOrigin: &types.ParametersInCacheKeyAndForwardedToOrigin{ CookiesConfig: &types.CachePolicyCookiesConfig{ CookieBehavior: types.CachePolicyCookieBehaviorAll, Cookies: &types.CookieNames{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "test-cookie", }, }, }, - EnableAcceptEncodingGzip: PtrBool(true), + EnableAcceptEncodingGzip: new(true), HeadersConfig: &types.CachePolicyHeadersConfig{ HeaderBehavior: types.CachePolicyHeaderBehaviorWhitelist, Headers: &types.Headers{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "test-header", }, @@ -42,13 +42,13 @@ var testCachePolicy = &types.CachePolicy{ QueryStringsConfig: &types.CachePolicyQueryStringsConfig{ QueryStringBehavior: types.CachePolicyQueryStringBehaviorWhitelist, QueryStrings: &types.QueryStringNames{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "test-query-string", }, }, }, - EnableAcceptEncodingBrotli: PtrBool(true), + EnableAcceptEncodingBrotli: new(true), }, }, } diff --git a/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go b/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go index 77c38358..1fe7ec00 100644 --- a/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go +++ b/aws-source/adapters/cloudfront-continuous-deployment-policy_test.go @@ -11,12 +11,12 @@ import ( func TestContinuousDeploymentPolicyItemMapper(t *testing.T) { item, err := continuousDeploymentPolicyItemMapper("", "test", &types.ContinuousDeploymentPolicy{ - Id: PtrString("test-id"), - LastModifiedTime: PtrTime(time.Now()), + Id: new("test-id"), + LastModifiedTime: new(time.Now()), ContinuousDeploymentPolicyConfig: &types.ContinuousDeploymentPolicyConfig{ - Enabled: PtrBool(true), + Enabled: new(true), StagingDistributionDnsNames: &types.StagingDistributionDnsNames{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "staging.test.com", // link }, @@ -24,14 +24,14 @@ func TestContinuousDeploymentPolicyItemMapper(t *testing.T) { TrafficConfig: &types.TrafficConfig{ Type: types.ContinuousDeploymentPolicyTypeSingleWeight, SingleHeaderConfig: &types.ContinuousDeploymentSingleHeaderConfig{ - Header: PtrString("test-header"), - Value: PtrString("test-value"), + Header: new("test-header"), + Value: new("test-value"), }, SingleWeightConfig: &types.ContinuousDeploymentSingleWeightConfig{ - Weight: PtrFloat32(1), + Weight: new(float32(1)), SessionStickinessConfig: &types.SessionStickinessConfig{ - IdleTTL: PtrInt32(1), - MaximumTTL: PtrInt32(2), + IdleTTL: new(int32(1)), + MaximumTTL: new(int32(2)), }, }, }, diff --git a/aws-source/adapters/cloudfront-distribution_test.go b/aws-source/adapters/cloudfront-distribution_test.go index 20c713df..3fd3db2a 100644 --- a/aws-source/adapters/cloudfront-distribution_test.go +++ b/aws-source/adapters/cloudfront-distribution_test.go @@ -14,20 +14,20 @@ import ( func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloudfront.GetDistributionInput, optFns ...func(*cloudfront.Options)) (*cloudfront.GetDistributionOutput, error) { return &cloudfront.GetDistributionOutput{ Distribution: &types.Distribution{ - ARN: PtrString("arn:aws:cloudfront::123456789012:distribution/test-id"), - DomainName: PtrString("d111111abcdef8.cloudfront.net"), // link - Id: PtrString("test-id"), - InProgressInvalidationBatches: PtrInt32(1), - LastModifiedTime: PtrTime(time.Now()), - Status: PtrString("Deployed"), // health: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-returned.html + ARN: new("arn:aws:cloudfront::123456789012:distribution/test-id"), + DomainName: new("d111111abcdef8.cloudfront.net"), // link + Id: new("test-id"), + InProgressInvalidationBatches: new(int32(1)), + LastModifiedTime: new(time.Now()), + Status: new("Deployed"), // health: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-returned.html ActiveTrustedKeyGroups: &types.ActiveTrustedKeyGroups{ - Enabled: PtrBool(true), - Quantity: PtrInt32(1), + Enabled: new(true), + Quantity: new(int32(1)), Items: []types.KGKeyPairIds{ { - KeyGroupId: PtrString("key-group-1"), // link + KeyGroupId: new("key-group-1"), // link KeyPairIds: &types.KeyPairIds{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "123456789", }, @@ -36,13 +36,13 @@ func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloud }, }, ActiveTrustedSigners: &types.ActiveTrustedSigners{ - Enabled: PtrBool(true), - Quantity: PtrInt32(1), + Enabled: new(true), + Quantity: new(int32(1)), Items: []types.Signer{ { - AwsAccountNumber: PtrString("123456789"), + AwsAccountNumber: new("123456789"), KeyPairIds: &types.KeyPairIds{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "123456789", }, @@ -52,54 +52,54 @@ func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloud }, AliasICPRecordals: []types.AliasICPRecordal{ { - CNAME: PtrString("something.foo.bar.com"), // link + CNAME: new("something.foo.bar.com"), // link ICPRecordalStatus: types.ICPRecordalStatusApproved, }, }, DistributionConfig: &types.DistributionConfig{ - CallerReference: PtrString("test-caller-reference"), - Comment: PtrString("test-comment"), - Enabled: PtrBool(true), + CallerReference: new("test-caller-reference"), + Comment: new("test-comment"), + Enabled: new(true), Aliases: &types.Aliases{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "www.example.com", // link }, }, - Staging: PtrBool(true), - ContinuousDeploymentPolicyId: PtrString("test-continuous-deployment-policy-id"), // link + Staging: new(true), + ContinuousDeploymentPolicyId: new("test-continuous-deployment-policy-id"), // link CacheBehaviors: &types.CacheBehaviors{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []types.CacheBehavior{ { - PathPattern: PtrString("/foo"), - TargetOriginId: PtrString("CustomOriginConfig"), + PathPattern: new("/foo"), + TargetOriginId: new("CustomOriginConfig"), ViewerProtocolPolicy: types.ViewerProtocolPolicyHttpsOnly, AllowedMethods: &types.AllowedMethods{ Items: []types.Method{ types.MethodGet, }, }, - CachePolicyId: PtrString("test-cache-policy-id"), // link - Compress: PtrBool(true), - DefaultTTL: PtrInt64(1), - FieldLevelEncryptionId: PtrString("test-field-level-encryption-id"), // link - MaxTTL: PtrInt64(1), - MinTTL: PtrInt64(1), - OriginRequestPolicyId: PtrString("test-origin-request-policy-id"), // link - RealtimeLogConfigArn: PtrString("arn:aws:logs:us-east-1:123456789012:realtime-log-config/test-id"), // link - ResponseHeadersPolicyId: PtrString("test-response-headers-policy-id"), // link - SmoothStreaming: PtrBool(true), + CachePolicyId: new("test-cache-policy-id"), // link + Compress: new(true), + DefaultTTL: new(int64(1)), + FieldLevelEncryptionId: new("test-field-level-encryption-id"), // link + MaxTTL: new(int64(1)), + MinTTL: new(int64(1)), + OriginRequestPolicyId: new("test-origin-request-policy-id"), // link + RealtimeLogConfigArn: new("arn:aws:logs:us-east-1:123456789012:realtime-log-config/test-id"), // link + ResponseHeadersPolicyId: new("test-response-headers-policy-id"), // link + SmoothStreaming: new(true), TrustedKeyGroups: &types.TrustedKeyGroups{ - Enabled: PtrBool(true), - Quantity: PtrInt32(1), + Enabled: new(true), + Quantity: new(int32(1)), Items: []string{ "key-group-1", // link }, }, TrustedSigners: &types.TrustedSigners{ - Enabled: PtrBool(true), - Quantity: PtrInt32(1), + Enabled: new(true), + Quantity: new(int32(1)), Items: []string{ "123456789", }, @@ -108,42 +108,42 @@ func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloud Cookies: &types.CookiePreference{ Forward: types.ItemSelectionWhitelist, WhitelistedNames: &types.CookieNames{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "cookie_123", }, }, }, - QueryString: PtrBool(true), + QueryString: new(true), Headers: &types.Headers{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "X-Customer-Header", }, }, QueryStringCacheKeys: &types.QueryStringCacheKeys{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "test-query-string-cache-key", }, }, }, FunctionAssociations: &types.FunctionAssociations{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []types.FunctionAssociation{ { EventType: types.EventTypeOriginRequest, - FunctionARN: PtrString("arn:aws:cloudfront::123412341234:function/1234"), // link + FunctionARN: new("arn:aws:cloudfront::123412341234:function/1234"), // link }, }, }, LambdaFunctionAssociations: &types.LambdaFunctionAssociations{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []types.LambdaFunctionAssociation{ { EventType: types.EventTypeOriginResponse, - LambdaFunctionARN: PtrString("arn:aws:lambda:us-east-1:123456789012:function:test-function"), // link - IncludeBody: PtrBool(true), + LambdaFunctionARN: new("arn:aws:lambda:us-east-1:123456789012:function:test-function"), // link + IncludeBody: new(true), }, }, }, @@ -153,107 +153,107 @@ func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloud Origins: &types.Origins{ Items: []types.Origin{ { - DomainName: PtrString("DOC-EXAMPLE-BUCKET.s3.us-west-2.amazonaws.com"), // link - Id: PtrString("CustomOriginConfig"), - ConnectionAttempts: PtrInt32(3), - ConnectionTimeout: PtrInt32(10), + DomainName: new("DOC-EXAMPLE-BUCKET.s3.us-west-2.amazonaws.com"), // link + Id: new("CustomOriginConfig"), + ConnectionAttempts: new(int32(3)), + ConnectionTimeout: new(int32(10)), CustomHeaders: &types.CustomHeaders{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []types.OriginCustomHeader{ { - HeaderName: PtrString("test-header-name"), - HeaderValue: PtrString("test-header-value"), + HeaderName: new("test-header-name"), + HeaderValue: new("test-header-value"), }, }, }, CustomOriginConfig: &types.CustomOriginConfig{ - HTTPPort: PtrInt32(80), - HTTPSPort: PtrInt32(443), + HTTPPort: new(int32(80)), + HTTPSPort: new(int32(443)), OriginProtocolPolicy: types.OriginProtocolPolicyMatchViewer, - OriginKeepaliveTimeout: PtrInt32(5), - OriginReadTimeout: PtrInt32(30), + OriginKeepaliveTimeout: new(int32(5)), + OriginReadTimeout: new(int32(30)), OriginSslProtocols: &types.OriginSslProtocols{ Items: types.SslProtocolSSLv3.Values(), }, }, - OriginAccessControlId: PtrString("test-origin-access-control-id"), // link - OriginPath: PtrString("/foo"), + OriginAccessControlId: new("test-origin-access-control-id"), // link + OriginPath: new("/foo"), OriginShield: &types.OriginShield{ - Enabled: PtrBool(true), - OriginShieldRegion: PtrString("eu-west-1"), + Enabled: new(true), + OriginShieldRegion: new("eu-west-1"), }, S3OriginConfig: &types.S3OriginConfig{ - OriginAccessIdentity: PtrString("test-origin-access-identity"), // link + OriginAccessIdentity: new("test-origin-access-identity"), // link }, }, }, }, DefaultCacheBehavior: &types.DefaultCacheBehavior{ - TargetOriginId: PtrString("CustomOriginConfig"), + TargetOriginId: new("CustomOriginConfig"), ViewerProtocolPolicy: types.ViewerProtocolPolicyHttpsOnly, - CachePolicyId: PtrString("test-cache-policy-id"), // link - Compress: PtrBool(true), - DefaultTTL: PtrInt64(1), - FieldLevelEncryptionId: PtrString("test-field-level-encryption-id"), // link - MaxTTL: PtrInt64(1), - MinTTL: PtrInt64(1), - OriginRequestPolicyId: PtrString("test-origin-request-policy-id"), // link - RealtimeLogConfigArn: PtrString("arn:aws:logs:us-east-1:123456789012:realtime-log-config/test-id"), // link - ResponseHeadersPolicyId: PtrString("test-response-headers-policy-id"), // link - SmoothStreaming: PtrBool(true), + CachePolicyId: new("test-cache-policy-id"), // link + Compress: new(true), + DefaultTTL: new(int64(1)), + FieldLevelEncryptionId: new("test-field-level-encryption-id"), // link + MaxTTL: new(int64(1)), + MinTTL: new(int64(1)), + OriginRequestPolicyId: new("test-origin-request-policy-id"), // link + RealtimeLogConfigArn: new("arn:aws:logs:us-east-1:123456789012:realtime-log-config/test-id"), // link + ResponseHeadersPolicyId: new("test-response-headers-policy-id"), // link + SmoothStreaming: new(true), ForwardedValues: &types.ForwardedValues{ Cookies: &types.CookiePreference{ Forward: types.ItemSelectionWhitelist, WhitelistedNames: &types.CookieNames{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "cooke_123", }, }, }, - QueryString: PtrBool(true), + QueryString: new(true), Headers: &types.Headers{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "X-Customer-Header", }, }, QueryStringCacheKeys: &types.QueryStringCacheKeys{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "test-query-string-cache-key", }, }, }, FunctionAssociations: &types.FunctionAssociations{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []types.FunctionAssociation{ { EventType: types.EventTypeViewerRequest, - FunctionARN: PtrString("arn:aws:cloudfront::123412341234:function/1234"), // link + FunctionARN: new("arn:aws:cloudfront::123412341234:function/1234"), // link }, }, }, LambdaFunctionAssociations: &types.LambdaFunctionAssociations{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []types.LambdaFunctionAssociation{ { EventType: types.EventTypeOriginRequest, - LambdaFunctionARN: PtrString("arn:aws:lambda:us-east-1:123456789012:function:test-function"), // link - IncludeBody: PtrBool(true), + LambdaFunctionARN: new("arn:aws:lambda:us-east-1:123456789012:function:test-function"), // link + IncludeBody: new(true), }, }, }, TrustedKeyGroups: &types.TrustedKeyGroups{ - Enabled: PtrBool(true), - Quantity: PtrInt32(1), + Enabled: new(true), + Quantity: new(int32(1)), Items: []string{ "key-group-1", // link }, }, TrustedSigners: &types.TrustedSigners{ - Enabled: PtrBool(true), - Quantity: PtrInt32(1), + Enabled: new(true), + Quantity: new(int32(1)), Items: []string{ "123456789", }, @@ -262,7 +262,7 @@ func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloud Items: []types.Method{ types.MethodGet, }, - Quantity: PtrInt32(1), + Quantity: new(int32(1)), CachedMethods: &types.CachedMethods{ Items: []types.Method{ types.MethodGet, @@ -271,27 +271,27 @@ func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloud }, }, CustomErrorResponses: &types.CustomErrorResponses{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []types.CustomErrorResponse{ { - ErrorCode: PtrInt32(404), - ErrorCachingMinTTL: PtrInt64(1), - ResponseCode: PtrString("200"), - ResponsePagePath: PtrString("/foo"), + ErrorCode: new(int32(404)), + ErrorCachingMinTTL: new(int64(1)), + ResponseCode: new("200"), + ResponsePagePath: new("/foo"), }, }, }, - DefaultRootObject: PtrString("index.html"), + DefaultRootObject: new("index.html"), HttpVersion: types.HttpVersionHttp11, - IsIPV6Enabled: PtrBool(true), + IsIPV6Enabled: new(true), Logging: &types.LoggingConfig{ - Bucket: PtrString("aws-cf-access-logs.s3.amazonaws.com"), // link - Enabled: PtrBool(true), - IncludeCookies: PtrBool(true), - Prefix: PtrString("test-prefix"), + Bucket: new("aws-cf-access-logs.s3.amazonaws.com"), // link + Enabled: new(true), + IncludeCookies: new(true), + Prefix: new("test-prefix"), }, OriginGroups: &types.OriginGroups{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []types.OriginGroup{ { FailoverCriteria: &types.OriginGroupFailoverCriteria{ @@ -299,15 +299,15 @@ func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloud Items: []int32{ 404, }, - Quantity: PtrInt32(1), + Quantity: new(int32(1)), }, }, - Id: PtrString("test-id"), + Id: new("test-id"), Members: &types.OriginGroupMembers{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []types.OriginGroupMember{ { - OriginId: PtrString("CustomOriginConfig"), + OriginId: new("CustomOriginConfig"), }, }, }, @@ -317,7 +317,7 @@ func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloud PriceClass: types.PriceClassPriceClass200, Restrictions: &types.Restrictions{ GeoRestriction: &types.GeoRestriction{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), RestrictionType: types.GeoRestrictionTypeWhitelist, Items: []string{ "US", @@ -325,16 +325,16 @@ func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloud }, }, ViewerCertificate: &types.ViewerCertificate{ - ACMCertificateArn: PtrString("arn:aws:acm:us-east-1:123456789012:certificate/test-id"), // link - Certificate: PtrString("test-certificate"), + ACMCertificateArn: new("arn:aws:acm:us-east-1:123456789012:certificate/test-id"), // link + Certificate: new("test-certificate"), CertificateSource: types.CertificateSourceAcm, - CloudFrontDefaultCertificate: PtrBool(true), - IAMCertificateId: PtrString("test-iam-certificate-id"), // link + CloudFrontDefaultCertificate: new(true), + IAMCertificateId: new("test-iam-certificate-id"), // link MinimumProtocolVersion: types.MinimumProtocolVersion(types.SslProtocolSSLv3), SSLSupportMethod: types.SSLSupportMethodSniOnly, }, // Note this can also be in the format: 473e64fd-f30b-4765-81a0-62ad96dd167a for WAF Classic - WebACLId: PtrString("arn:aws:wafv2:us-east-1:123456789012:global/webacl/ExampleWebACL/473e64fd-f30b-4765-81a0-62ad96dd167a"), // link + WebACLId: new("arn:aws:wafv2:us-east-1:123456789012:global/webacl/ExampleWebACL/473e64fd-f30b-4765-81a0-62ad96dd167a"), // link }, }, }, nil @@ -343,10 +343,10 @@ func (t TestCloudFrontClient) GetDistribution(ctx context.Context, params *cloud func (t TestCloudFrontClient) ListDistributions(ctx context.Context, params *cloudfront.ListDistributionsInput, optFns ...func(*cloudfront.Options)) (*cloudfront.ListDistributionsOutput, error) { return &cloudfront.ListDistributionsOutput{ DistributionList: &types.DistributionList{ - IsTruncated: PtrBool(false), + IsTruncated: new(false), Items: []types.DistributionSummary{ { - Id: PtrString("test-id"), + Id: new("test-id"), }, }, }, diff --git a/aws-source/adapters/cloudfront-function_test.go b/aws-source/adapters/cloudfront-function_test.go index 83f3fe9b..29c12b89 100644 --- a/aws-source/adapters/cloudfront-function_test.go +++ b/aws-source/adapters/cloudfront-function_test.go @@ -11,17 +11,17 @@ import ( func TestFunctionItemMapper(t *testing.T) { summary := types.FunctionSummary{ FunctionConfig: &types.FunctionConfig{ - Comment: PtrString("test-comment"), + Comment: new("test-comment"), Runtime: types.FunctionRuntimeCloudfrontJs20, }, FunctionMetadata: &types.FunctionMetadata{ - FunctionARN: PtrString("arn:aws:cloudfront::123456789012:function/test-function"), - LastModifiedTime: PtrTime(time.Now()), - CreatedTime: PtrTime(time.Now()), + FunctionARN: new("arn:aws:cloudfront::123456789012:function/test-function"), + LastModifiedTime: new(time.Now()), + CreatedTime: new(time.Now()), Stage: types.FunctionStageLive, }, - Name: PtrString("test-function"), - Status: PtrString("test-status"), + Name: new("test-function"), + Status: new("test-status"), } item, err := functionItemMapper("", "test", &summary) diff --git a/aws-source/adapters/cloudfront-key-group_test.go b/aws-source/adapters/cloudfront-key-group_test.go index c874fae0..ebf69b5e 100644 --- a/aws-source/adapters/cloudfront-key-group_test.go +++ b/aws-source/adapters/cloudfront-key-group_test.go @@ -10,15 +10,15 @@ import ( func TestKeyGroupItemMapper(t *testing.T) { group := types.KeyGroup{ - Id: PtrString("test-id"), + Id: new("test-id"), KeyGroupConfig: &types.KeyGroupConfig{ Items: []string{ "some-identity", }, - Name: PtrString("test-name"), - Comment: PtrString("test-comment"), + Name: new("test-name"), + Comment: new("test-comment"), }, - LastModifiedTime: PtrTime(time.Now()), + LastModifiedTime: new(time.Now()), } item, err := KeyGroupItemMapper("", "test", &group) diff --git a/aws-source/adapters/cloudfront-origin-access-control_test.go b/aws-source/adapters/cloudfront-origin-access-control_test.go index 25621cce..5c78db94 100644 --- a/aws-source/adapters/cloudfront-origin-access-control_test.go +++ b/aws-source/adapters/cloudfront-origin-access-control_test.go @@ -10,13 +10,13 @@ import ( func TestOriginAccessControlItemMapper(t *testing.T) { x := types.OriginAccessControl{ - Id: PtrString("test"), + Id: new("test"), OriginAccessControlConfig: &types.OriginAccessControlConfig{ - Name: PtrString("example-name"), + Name: new("example-name"), OriginAccessControlOriginType: types.OriginAccessControlOriginTypesS3, SigningBehavior: types.OriginAccessControlSigningBehaviorsAlways, SigningProtocol: types.OriginAccessControlSigningProtocolsSigv4, - Description: PtrString("example-description"), + Description: new("example-description"), }, } diff --git a/aws-source/adapters/cloudfront-origin-request-policy_test.go b/aws-source/adapters/cloudfront-origin-request-policy_test.go index 4458f998..0e5f639d 100644 --- a/aws-source/adapters/cloudfront-origin-request-policy_test.go +++ b/aws-source/adapters/cloudfront-origin-request-policy_test.go @@ -10,29 +10,29 @@ import ( func TestOriginRequestPolicyItemMapper(t *testing.T) { x := types.OriginRequestPolicy{ - Id: PtrString("test"), - LastModifiedTime: PtrTime(time.Now()), + Id: new("test"), + LastModifiedTime: new(time.Now()), OriginRequestPolicyConfig: &types.OriginRequestPolicyConfig{ - Name: PtrString("example-policy"), - Comment: PtrString("example comment"), + Name: new("example-policy"), + Comment: new("example comment"), QueryStringsConfig: &types.OriginRequestPolicyQueryStringsConfig{ QueryStringBehavior: types.OriginRequestPolicyQueryStringBehaviorAllExcept, QueryStrings: &types.QueryStringNames{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{"test"}, }, }, CookiesConfig: &types.OriginRequestPolicyCookiesConfig{ CookieBehavior: types.OriginRequestPolicyCookieBehaviorAll, Cookies: &types.CookieNames{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{"test"}, }, }, HeadersConfig: &types.OriginRequestPolicyHeadersConfig{ HeaderBehavior: types.OriginRequestPolicyHeaderBehaviorAllViewer, Headers: &types.Headers{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{"test"}, }, }, diff --git a/aws-source/adapters/cloudfront-realtime-log-config_test.go b/aws-source/adapters/cloudfront-realtime-log-config_test.go index 58ae1589..c5fb0587 100644 --- a/aws-source/adapters/cloudfront-realtime-log-config_test.go +++ b/aws-source/adapters/cloudfront-realtime-log-config_test.go @@ -11,15 +11,15 @@ import ( func TestRealtimeLogConfigsItemMapper(t *testing.T) { x := types.RealtimeLogConfig{ - Name: PtrString("test"), - SamplingRate: PtrInt64(100), - ARN: PtrString("arn:aws:cloudfront::123456789012:realtime-log-config/12345678-1234-1234-1234-123456789012"), + Name: new("test"), + SamplingRate: new(int64(100)), + ARN: new("arn:aws:cloudfront::123456789012:realtime-log-config/12345678-1234-1234-1234-123456789012"), EndPoints: []types.EndPoint{ { - StreamType: PtrString("Kinesis"), + StreamType: new("Kinesis"), KinesisStreamConfig: &types.KinesisStreamConfig{ - RoleARN: PtrString("arn:aws:iam::123456789012:role/CloudFront_Logger"), // link - StreamARN: PtrString("arn:aws:kinesis:us-east-1:123456789012:stream/cloudfront-logs"), // link + RoleARN: new("arn:aws:iam::123456789012:role/CloudFront_Logger"), // link + StreamARN: new("arn:aws:kinesis:us-east-1:123456789012:stream/cloudfront-logs"), // link }, }, }, diff --git a/aws-source/adapters/cloudfront-response-headers-policy_test.go b/aws-source/adapters/cloudfront-response-headers-policy_test.go index e188964a..2806b9a7 100644 --- a/aws-source/adapters/cloudfront-response-headers-policy_test.go +++ b/aws-source/adapters/cloudfront-response-headers-policy_test.go @@ -10,68 +10,68 @@ import ( func TestResponseHeadersPolicyItemMapper(t *testing.T) { x := types.ResponseHeadersPolicy{ - Id: PtrString("test"), - LastModifiedTime: PtrTime(time.Now()), + Id: new("test"), + LastModifiedTime: new(time.Now()), ResponseHeadersPolicyConfig: &types.ResponseHeadersPolicyConfig{ - Name: PtrString("example-policy"), - Comment: PtrString("example comment"), + Name: new("example-policy"), + Comment: new("example comment"), CorsConfig: &types.ResponseHeadersPolicyCorsConfig{ - AccessControlAllowCredentials: PtrBool(true), + AccessControlAllowCredentials: new(true), AccessControlAllowHeaders: &types.ResponseHeadersPolicyAccessControlAllowHeaders{ Items: []string{"X-Customer-Header"}, - Quantity: PtrInt32(1), + Quantity: new(int32(1)), }, }, CustomHeadersConfig: &types.ResponseHeadersPolicyCustomHeadersConfig{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []types.ResponseHeadersPolicyCustomHeader{ { - Header: PtrString("X-Customer-Header"), - Override: PtrBool(true), - Value: PtrString("test"), + Header: new("X-Customer-Header"), + Override: new(true), + Value: new("test"), }, }, }, RemoveHeadersConfig: &types.ResponseHeadersPolicyRemoveHeadersConfig{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []types.ResponseHeadersPolicyRemoveHeader{ { - Header: PtrString("X-Private-Header"), + Header: new("X-Private-Header"), }, }, }, SecurityHeadersConfig: &types.ResponseHeadersPolicySecurityHeadersConfig{ ContentSecurityPolicy: &types.ResponseHeadersPolicyContentSecurityPolicy{ - ContentSecurityPolicy: PtrString("default-src 'none';"), - Override: PtrBool(true), + ContentSecurityPolicy: new("default-src 'none';"), + Override: new(true), }, ContentTypeOptions: &types.ResponseHeadersPolicyContentTypeOptions{ - Override: PtrBool(true), + Override: new(true), }, FrameOptions: &types.ResponseHeadersPolicyFrameOptions{ FrameOption: types.FrameOptionsListDeny, - Override: PtrBool(true), + Override: new(true), }, ReferrerPolicy: &types.ResponseHeadersPolicyReferrerPolicy{ - Override: PtrBool(true), + Override: new(true), ReferrerPolicy: types.ReferrerPolicyListNoReferrer, }, StrictTransportSecurity: &types.ResponseHeadersPolicyStrictTransportSecurity{ - AccessControlMaxAgeSec: PtrInt32(86400), - Override: PtrBool(true), - IncludeSubdomains: PtrBool(true), - Preload: PtrBool(true), + AccessControlMaxAgeSec: new(int32(86400)), + Override: new(true), + IncludeSubdomains: new(true), + Preload: new(true), }, XSSProtection: &types.ResponseHeadersPolicyXSSProtection{ - Override: PtrBool(true), - Protection: PtrBool(true), - ModeBlock: PtrBool(true), - ReportUri: PtrString("https://example.com/report"), + Override: new(true), + Protection: new(true), + ModeBlock: new(true), + ReportUri: new("https://example.com/report"), }, }, ServerTimingHeadersConfig: &types.ResponseHeadersPolicyServerTimingHeadersConfig{ - Enabled: PtrBool(true), - SamplingRate: PtrFloat64(0.1), + Enabled: new(true), + SamplingRate: new(0.1), }, }, } diff --git a/aws-source/adapters/cloudfront-streaming-distribution_test.go b/aws-source/adapters/cloudfront-streaming-distribution_test.go index eaeb0502..e211c389 100644 --- a/aws-source/adapters/cloudfront-streaming-distribution_test.go +++ b/aws-source/adapters/cloudfront-streaming-distribution_test.go @@ -13,21 +13,21 @@ import ( func (t TestCloudFrontClient) GetStreamingDistribution(ctx context.Context, params *cloudfront.GetStreamingDistributionInput, optFns ...func(*cloudfront.Options)) (*cloudfront.GetStreamingDistributionOutput, error) { return &cloudfront.GetStreamingDistributionOutput{ - ETag: PtrString("E2QWRUHAPOMQZL"), + ETag: new("E2QWRUHAPOMQZL"), StreamingDistribution: &types.StreamingDistribution{ - ARN: PtrString("arn:aws:cloudfront::123456789012:streaming-distribution/EDFDVBD632BHDS5"), - DomainName: PtrString("d111111abcdef8.cloudfront.net"), // link - Id: PtrString("EDFDVBD632BHDS5"), - Status: PtrString("Deployed"), // health - LastModifiedTime: PtrTime(time.Now()), + ARN: new("arn:aws:cloudfront::123456789012:streaming-distribution/EDFDVBD632BHDS5"), + DomainName: new("d111111abcdef8.cloudfront.net"), // link + Id: new("EDFDVBD632BHDS5"), + Status: new("Deployed"), // health + LastModifiedTime: new(time.Now()), ActiveTrustedSigners: &types.ActiveTrustedSigners{ - Enabled: PtrBool(true), - Quantity: PtrInt32(1), + Enabled: new(true), + Quantity: new(int32(1)), Items: []types.Signer{ { - AwsAccountNumber: PtrString("123456789012"), + AwsAccountNumber: new("123456789012"), KeyPairIds: &types.KeyPairIds{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "APKAJDGKZRVEXAMPLE", }, @@ -36,30 +36,30 @@ func (t TestCloudFrontClient) GetStreamingDistribution(ctx context.Context, para }, }, StreamingDistributionConfig: &types.StreamingDistributionConfig{ - CallerReference: PtrString("test"), - Comment: PtrString("test"), - Enabled: PtrBool(true), + CallerReference: new("test"), + Comment: new("test"), + Enabled: new(true), S3Origin: &types.S3Origin{ - DomainName: PtrString("myawsbucket.s3.amazonaws.com"), // link - OriginAccessIdentity: PtrString("origin-access-identity/cloudfront/E127EXAMPLE51Z"), // link + DomainName: new("myawsbucket.s3.amazonaws.com"), // link + OriginAccessIdentity: new("origin-access-identity/cloudfront/E127EXAMPLE51Z"), // link }, TrustedSigners: &types.TrustedSigners{ - Enabled: PtrBool(true), - Quantity: PtrInt32(1), + Enabled: new(true), + Quantity: new(int32(1)), Items: []string{ "self", }, }, Aliases: &types.Aliases{ - Quantity: PtrInt32(1), + Quantity: new(int32(1)), Items: []string{ "example.com", // link }, }, Logging: &types.StreamingLoggingConfig{ - Bucket: PtrString("myawslogbucket.s3.amazonaws.com"), // link - Enabled: PtrBool(true), - Prefix: PtrString("myprefix"), + Bucket: new("myawslogbucket.s3.amazonaws.com"), // link + Enabled: new(true), + Prefix: new("myprefix"), }, PriceClass: types.PriceClassPriceClassAll, }, @@ -70,10 +70,10 @@ func (t TestCloudFrontClient) GetStreamingDistribution(ctx context.Context, para func (t TestCloudFrontClient) ListStreamingDistributions(ctx context.Context, params *cloudfront.ListStreamingDistributionsInput, optFns ...func(*cloudfront.Options)) (*cloudfront.ListStreamingDistributionsOutput, error) { return &cloudfront.ListStreamingDistributionsOutput{ StreamingDistributionList: &types.StreamingDistributionList{ - IsTruncated: PtrBool(false), + IsTruncated: new(false), Items: []types.StreamingDistributionSummary{ { - Id: PtrString("test-id"), + Id: new("test-id"), }, }, }, diff --git a/aws-source/adapters/cloudfront_test.go b/aws-source/adapters/cloudfront_test.go index 7486fe9d..3357e79d 100644 --- a/aws-source/adapters/cloudfront_test.go +++ b/aws-source/adapters/cloudfront_test.go @@ -13,8 +13,8 @@ func (c TestCloudFrontClient) ListTagsForResource(ctx context.Context, params *c Tags: &types.Tags{ Items: []types.Tag{ { - Key: PtrString("foo"), - Value: PtrString("bar"), + Key: new("foo"), + Value: new("bar"), }, }, }, diff --git a/aws-source/adapters/cloudwatch-alarm_test.go b/aws-source/adapters/cloudwatch-alarm_test.go index be179f7e..2af7a602 100644 --- a/aws-source/adapters/cloudwatch-alarm_test.go +++ b/aws-source/adapters/cloudwatch-alarm_test.go @@ -19,8 +19,8 @@ func (c testCloudwatchClient) ListTagsForResource(ctx context.Context, params *c return &cloudwatch.ListTagsForResourceOutput{ Tags: []types.Tag{ { - Key: PtrString("Name"), - Value: PtrString("example"), + Key: new("Name"), + Value: new("example"), }, }, }, nil @@ -38,11 +38,11 @@ func TestAlarmOutputMapper(t *testing.T) { output := &cloudwatch.DescribeAlarmsOutput{ MetricAlarms: []types.MetricAlarm{ { - AlarmName: PtrString("TargetTracking-table/dylan-tfstate-AlarmHigh-14069c4a-6dcc-48a2-bfe6-b5547c90c43d"), - AlarmArn: PtrString("arn:aws:cloudwatch:eu-west-2:052392120703:alarm:TargetTracking-table/dylan-tfstate-AlarmHigh-14069c4a-6dcc-48a2-bfe6-b5547c90c43d"), - AlarmDescription: PtrString("DO NOT EDIT OR DELETE. For TargetTrackingScaling policy arn:aws:autoscaling:eu-west-2:052392120703:scalingPolicy:32f3f053-dc75-46fa-9cd4-8e8c34c47b37:resource/dynamodb/table/dylan-tfstate:policyName/$dylan-tfstate-scaling-policy:createdBy/e5bd51d8-94a8-461e-a989-08f4d10b326b."), - AlarmConfigurationUpdatedTimestamp: PtrTime(time.Now()), - ActionsEnabled: PtrBool(true), + AlarmName: new("TargetTracking-table/dylan-tfstate-AlarmHigh-14069c4a-6dcc-48a2-bfe6-b5547c90c43d"), + AlarmArn: new("arn:aws:cloudwatch:eu-west-2:052392120703:alarm:TargetTracking-table/dylan-tfstate-AlarmHigh-14069c4a-6dcc-48a2-bfe6-b5547c90c43d"), + AlarmDescription: new("DO NOT EDIT OR DELETE. For TargetTrackingScaling policy arn:aws:autoscaling:eu-west-2:052392120703:scalingPolicy:32f3f053-dc75-46fa-9cd4-8e8c34c47b37:resource/dynamodb/table/dylan-tfstate:policyName/$dylan-tfstate-scaling-policy:createdBy/e5bd51d8-94a8-461e-a989-08f4d10b326b."), + AlarmConfigurationUpdatedTimestamp: new(time.Now()), + ActionsEnabled: new(true), OKActions: []string{ "arn:aws:autoscaling:eu-west-2:052392120703:scalingPolicy:32f3f053-dc75-46fa-9cd4-8e8c34c47b37:resource/dynamodb/table/dylan-tfstate:policyName/$dylan-tfstate-scaling-policy:createdBy/e5bd51d8-94a8-461e-a989-08f4d10b326b", }, @@ -53,32 +53,32 @@ func TestAlarmOutputMapper(t *testing.T) { "arn:aws:autoscaling:eu-west-2:052392120703:scalingPolicy:32f3f053-dc75-46fa-9cd4-8e8c34c47b37:resource/dynamodb/table/dylan-tfstate:policyName/$dylan-tfstate-scaling-policy:createdBy/e5bd51d8-94a8-461e-a989-08f4d10b326b", }, StateValue: types.StateValueOk, - StateReason: PtrString("Threshold Crossed: 2 datapoints [0.0 (09/01/23 14:02:00), 1.0 (09/01/23 14:01:00)] were not greater than the threshold (42.0)."), - StateReasonData: PtrString("{\"version\":\"1.0\",\"queryDate\":\"2023-01-09T14:07:25.504+0000\",\"startDate\":\"2023-01-09T14:01:00.000+0000\",\"statistic\":\"Sum\",\"period\":60,\"recentDatapoints\":[1.0,0.0],\"threshold\":42.0,\"evaluatedDatapoints\":[{\"timestamp\":\"2023-01-09T14:02:00.000+0000\",\"sampleCount\":1.0,\"value\":0.0}]}"), - StateUpdatedTimestamp: PtrTime(time.Now()), - MetricName: PtrString("ConsumedWriteCapacityUnits"), - Namespace: PtrString("AWS/DynamoDB"), + StateReason: new("Threshold Crossed: 2 datapoints [0.0 (09/01/23 14:02:00), 1.0 (09/01/23 14:01:00)] were not greater than the threshold (42.0)."), + StateReasonData: new("{\"version\":\"1.0\",\"queryDate\":\"2023-01-09T14:07:25.504+0000\",\"startDate\":\"2023-01-09T14:01:00.000+0000\",\"statistic\":\"Sum\",\"period\":60,\"recentDatapoints\":[1.0,0.0],\"threshold\":42.0,\"evaluatedDatapoints\":[{\"timestamp\":\"2023-01-09T14:02:00.000+0000\",\"sampleCount\":1.0,\"value\":0.0}]}"), + StateUpdatedTimestamp: new(time.Now()), + MetricName: new("ConsumedWriteCapacityUnits"), + Namespace: new("AWS/DynamoDB"), Statistic: types.StatisticSum, Dimensions: []types.Dimension{ { - Name: PtrString("TableName"), - Value: PtrString("dylan-tfstate"), + Name: new("TableName"), + Value: new("dylan-tfstate"), }, }, - Period: PtrInt32(60), - EvaluationPeriods: PtrInt32(2), - Threshold: PtrFloat64(42.0), + Period: new(int32(60)), + EvaluationPeriods: new(int32(2)), + Threshold: new(42.0), ComparisonOperator: types.ComparisonOperatorGreaterThanThreshold, - StateTransitionedTimestamp: PtrTime(time.Now()), + StateTransitionedTimestamp: new(time.Now()), }, }, CompositeAlarms: []types.CompositeAlarm{ { - AlarmName: PtrString("TargetTracking2-table/dylan-tfstate-AlarmHigh-14069c4a-6dcc-48a2-bfe6-b5547c90c43d"), - AlarmArn: PtrString("arn:aws:cloudwatch:eu-west-2:052392120703:alarm:TargetTracking2-table/dylan-tfstate-AlarmHigh-14069c4a-6dcc-48a2-bfe6-b5547c90c43d"), - AlarmDescription: PtrString("DO NOT EDIT OR DELETE. For TargetTrackingScaling policy arn:aws:autoscaling:eu-west-2:052392120703:scalingPolicy:32f3f053-dc75-46fa-9cd4-8e8c34c47b37:resource/dynamodb/table/dylan-tfstate:policyName/$dylan-tfstate-scaling-policy:createdBy/e5bd51d8-94a8-461e-a989-08f4d10b326b."), - AlarmConfigurationUpdatedTimestamp: PtrTime(time.Now()), - ActionsEnabled: PtrBool(true), + AlarmName: new("TargetTracking2-table/dylan-tfstate-AlarmHigh-14069c4a-6dcc-48a2-bfe6-b5547c90c43d"), + AlarmArn: new("arn:aws:cloudwatch:eu-west-2:052392120703:alarm:TargetTracking2-table/dylan-tfstate-AlarmHigh-14069c4a-6dcc-48a2-bfe6-b5547c90c43d"), + AlarmDescription: new("DO NOT EDIT OR DELETE. For TargetTrackingScaling policy arn:aws:autoscaling:eu-west-2:052392120703:scalingPolicy:32f3f053-dc75-46fa-9cd4-8e8c34c47b37:resource/dynamodb/table/dylan-tfstate:policyName/$dylan-tfstate-scaling-policy:createdBy/e5bd51d8-94a8-461e-a989-08f4d10b326b."), + AlarmConfigurationUpdatedTimestamp: new(time.Now()), + ActionsEnabled: new(true), OKActions: []string{ "arn:aws:autoscaling:eu-west-2:052392120703:scalingPolicy:32f3f053-dc75-46fa-9cd4-8e8c34c47b37:resource/dynamodb/table/dylan-tfstate:policyName/$dylan-tfstate-scaling-policy:createdBy/e5bd51d8-94a8-461e-a989-08f4d10b326b", }, @@ -89,17 +89,17 @@ func TestAlarmOutputMapper(t *testing.T) { "arn:aws:autoscaling:eu-west-2:052392120703:scalingPolicy:32f3f053-dc75-46fa-9cd4-8e8c34c47b37:resource/dynamodb/table/dylan-tfstate:policyName/$dylan-tfstate-scaling-policy:createdBy/e5bd51d8-94a8-461e-a989-08f4d10b326b", }, StateValue: types.StateValueOk, - StateReason: PtrString("Threshold Crossed: 2 datapoints [0.0 (09/01/23 14:02:00), 1.0 (09/01/23 14:01:00)] were not greater than the threshold (42.0)."), - StateReasonData: PtrString("{\"version\":\"1.0\",\"queryDate\":\"2023-01-09T14:07:25.504+0000\",\"startDate\":\"2023-01-09T14:01:00.000+0000\",\"statistic\":\"Sum\",\"period\":60,\"recentDatapoints\":[1.0,0.0],\"threshold\":42.0,\"evaluatedDatapoints\":[{\"timestamp\":\"2023-01-09T14:02:00.000+0000\",\"sampleCount\":1.0,\"value\":0.0}]}"), - StateUpdatedTimestamp: PtrTime(time.Now()), - StateTransitionedTimestamp: PtrTime(time.Now()), + StateReason: new("Threshold Crossed: 2 datapoints [0.0 (09/01/23 14:02:00), 1.0 (09/01/23 14:01:00)] were not greater than the threshold (42.0)."), + StateReasonData: new("{\"version\":\"1.0\",\"queryDate\":\"2023-01-09T14:07:25.504+0000\",\"startDate\":\"2023-01-09T14:01:00.000+0000\",\"statistic\":\"Sum\",\"period\":60,\"recentDatapoints\":[1.0,0.0],\"threshold\":42.0,\"evaluatedDatapoints\":[{\"timestamp\":\"2023-01-09T14:02:00.000+0000\",\"sampleCount\":1.0,\"value\":0.0}]}"), + StateUpdatedTimestamp: new(time.Now()), + StateTransitionedTimestamp: new(time.Now()), ActionsSuppressedBy: types.ActionsSuppressedByAlarm, - ActionsSuppressedReason: PtrString("Alarm is in INSUFFICIENT_DATA state"), + ActionsSuppressedReason: new("Alarm is in INSUFFICIENT_DATA state"), // link - ActionsSuppressor: PtrString("arn:aws:cloudwatch:eu-west-2:052392120703:alarm:TargetTracking2-table/dylan-tfstate-AlarmHigh-14069c4a-6dcc-48a2-bfe6-b5547c90c43d"), - ActionsSuppressorExtensionPeriod: PtrInt32(0), - ActionsSuppressorWaitPeriod: PtrInt32(0), - AlarmRule: PtrString("ALARM TargetTracking2-table/dylan-tfstate-AlarmHigh-14069c4a-6dcc-48a2-bfe6-b5547c90c43d"), + ActionsSuppressor: new("arn:aws:cloudwatch:eu-west-2:052392120703:alarm:TargetTracking2-table/dylan-tfstate-AlarmHigh-14069c4a-6dcc-48a2-bfe6-b5547c90c43d"), + ActionsSuppressorExtensionPeriod: new(int32(0)), + ActionsSuppressorWaitPeriod: new(int32(0)), + AlarmRule: new("ALARM TargetTracking2-table/dylan-tfstate-AlarmHigh-14069c4a-6dcc-48a2-bfe6-b5547c90c43d"), }, }, } @@ -178,12 +178,12 @@ func TestAlarmOutputMapperWithTagError(t *testing.T) { output := &cloudwatch.DescribeAlarmsOutput{ MetricAlarms: []types.MetricAlarm{ { - AlarmName: PtrString("api-51c748b4-cpu-credits-low"), - AlarmArn: PtrString("arn:aws:cloudwatch:eu-west-2:052392120703:alarm:api-51c748b4-cpu-credits-low"), - AlarmDescription: PtrString("CPU credits low alarm"), + AlarmName: new("api-51c748b4-cpu-credits-low"), + AlarmArn: new("arn:aws:cloudwatch:eu-west-2:052392120703:alarm:api-51c748b4-cpu-credits-low"), + AlarmDescription: new("CPU credits low alarm"), StateValue: types.StateValueOk, - MetricName: PtrString("CPUCreditBalance"), - Namespace: PtrString("AWS/EC2"), + MetricName: new("CPUCreditBalance"), + Namespace: new("AWS/EC2"), }, }, } diff --git a/aws-source/adapters/cloudwatch-instance-metric.go b/aws-source/adapters/cloudwatch-instance-metric.go index ef9a0e6a..c43b96a4 100644 --- a/aws-source/adapters/cloudwatch-instance-metric.go +++ b/aws-source/adapters/cloudwatch-instance-metric.go @@ -121,7 +121,7 @@ func formatMetricValue(metricName string, value float64) string { // metricOutputMapper converts CloudWatch GetMetricData output to an SDP item func metricOutputMapper(ctx context.Context, client CloudwatchMetricClient, scope string, instanceID string, output *cloudwatch.GetMetricDataOutput) (*sdp.Item, error) { // Build attributes map with instance ID - attrsMap := map[string]interface{}{ + attrsMap := map[string]any{ "InstanceId": instanceID, "PeriodMinutes": 15, "Statistic": "Average", diff --git a/aws-source/adapters/directconnect-connection_test.go b/aws-source/adapters/directconnect-connection_test.go index 86dc2d8a..31d546e6 100644 --- a/aws-source/adapters/directconnect-connection_test.go +++ b/aws-source/adapters/directconnect-connection_test.go @@ -15,26 +15,26 @@ func TestDirectconnectConnectionOutputMapper(t *testing.T) { output := &directconnect.DescribeConnectionsOutput{ Connections: []types.Connection{ { - AwsDeviceV2: PtrString("EqDC2-123h49s71dabc"), - AwsLogicalDeviceId: PtrString("device-1"), - Bandwidth: PtrString("1Gbps"), - ConnectionId: PtrString("dxcon-fguhmqlc"), - ConnectionName: PtrString("My_Connection"), + AwsDeviceV2: new("EqDC2-123h49s71dabc"), + AwsLogicalDeviceId: new("device-1"), + Bandwidth: new("1Gbps"), + ConnectionId: new("dxcon-fguhmqlc"), + ConnectionName: new("My_Connection"), ConnectionState: "down", - EncryptionMode: PtrString("must_encrypt"), + EncryptionMode: new("must_encrypt"), HasLogicalRedundancy: "unknown", - JumboFrameCapable: PtrBool(true), - LagId: PtrString("dxlag-ffrz71kw"), - LoaIssueTime: PtrTime(time.Now()), - Location: PtrString("EqDC2"), - Region: PtrString("us-east-1"), - ProviderName: PtrString("provider-1"), - OwnerAccount: PtrString("123456789012"), - PartnerName: PtrString("partner-1"), + JumboFrameCapable: new(true), + LagId: new("dxlag-ffrz71kw"), + LoaIssueTime: new(time.Now()), + Location: new("EqDC2"), + Region: new("us-east-1"), + ProviderName: new("provider-1"), + OwnerAccount: new("123456789012"), + PartnerName: new("partner-1"), Tags: []types.Tag{ { - Key: PtrString("foo"), - Value: PtrString("bar"), + Key: new("foo"), + Value: new("bar"), }, }, }, diff --git a/aws-source/adapters/directconnect-customer-metadata_test.go b/aws-source/adapters/directconnect-customer-metadata_test.go index f2375df6..616589f3 100644 --- a/aws-source/adapters/directconnect-customer-metadata_test.go +++ b/aws-source/adapters/directconnect-customer-metadata_test.go @@ -14,8 +14,8 @@ func TestCustomerMetadataOutputMapper(t *testing.T) { output := &directconnect.DescribeCustomerMetadataOutput{ Agreements: []types.CustomerAgreement{ { - AgreementName: PtrString("example-customer-agreement"), - Status: PtrString("signed"), + AgreementName: new("example-customer-agreement"), + Status: new("signed"), }, }, } 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 942eff81..c540f6b7 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 @@ -17,27 +17,27 @@ func TestDirectConnectGatewayAssociationProposalOutputMapper(t *testing.T) { output := &directconnect.DescribeDirectConnectGatewayAssociationProposalsOutput{ DirectConnectGatewayAssociationProposals: []types.DirectConnectGatewayAssociationProposal{ { - ProposalId: PtrString("c2ede9b4-bbc6-4d33-923c-bc4feEXAMPLE"), - DirectConnectGatewayId: PtrString("5f294f92-bafb-4011-916d-9b0bexample"), - DirectConnectGatewayOwnerAccount: PtrString("123456789012"), + ProposalId: new("c2ede9b4-bbc6-4d33-923c-bc4feEXAMPLE"), + DirectConnectGatewayId: new("5f294f92-bafb-4011-916d-9b0bexample"), + DirectConnectGatewayOwnerAccount: new("123456789012"), ProposalState: types.DirectConnectGatewayAssociationProposalStateRequested, AssociatedGateway: &types.AssociatedGateway{ - Id: PtrString("tgw-02f776b1a7EXAMPLE"), + Id: new("tgw-02f776b1a7EXAMPLE"), Type: types.GatewayTypeTransitGateway, - OwnerAccount: PtrString("111122223333"), - Region: PtrString("us-east-1"), + OwnerAccount: new("111122223333"), + Region: new("us-east-1"), }, ExistingAllowedPrefixesToDirectConnectGateway: []types.RouteFilterPrefix{ { - Cidr: PtrString("192.168.2.0/30"), + Cidr: new("192.168.2.0/30"), }, { - Cidr: PtrString("192.168.1.0/30"), + Cidr: new("192.168.1.0/30"), }, }, RequestedAllowedPrefixesToDirectConnectGateway: []types.RouteFilterPrefix{ { - Cidr: PtrString("192.168.1.0/30"), + Cidr: new("192.168.1.0/30"), }, }, }, 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 7fc04408..226eebef 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-association_test.go @@ -16,10 +16,10 @@ func TestDirectConnectGatewayAssociationOutputMapper_Health_OK(t *testing.T) { DirectConnectGatewayAssociations: []types.DirectConnectGatewayAssociation{ { AssociationState: types.DirectConnectGatewayAssociationStateAssociating, - AssociationId: PtrString("cf68415c-f4ae-48f2-87a7-3b52cexample"), - VirtualGatewayOwnerAccount: PtrString("123456789012"), - DirectConnectGatewayId: PtrString("5f294f92-bafb-4011-916d-9b0bexample"), - VirtualGatewayId: PtrString("vgw-6efe725e"), + AssociationId: new("cf68415c-f4ae-48f2-87a7-3b52cexample"), + VirtualGatewayOwnerAccount: new("123456789012"), + DirectConnectGatewayId: new("5f294f92-bafb-4011-916d-9b0bexample"), + VirtualGatewayId: new("vgw-6efe725e"), }, }, } @@ -68,11 +68,11 @@ func TestDirectConnectGatewayAssociationOutputMapper_Health_Error(t *testing.T) DirectConnectGatewayAssociations: []types.DirectConnectGatewayAssociation{ { AssociationState: types.DirectConnectGatewayAssociationStateAssociating, - AssociationId: PtrString("cf68415c-f4ae-48f2-87a7-3b52cexample"), - VirtualGatewayOwnerAccount: PtrString("123456789012"), - DirectConnectGatewayId: PtrString("5f294f92-bafb-4011-916d-9b0bexample"), - VirtualGatewayId: PtrString("vgw-6efe725e"), - StateChangeError: PtrString("something went wrong"), + AssociationId: new("cf68415c-f4ae-48f2-87a7-3b52cexample"), + VirtualGatewayOwnerAccount: new("123456789012"), + DirectConnectGatewayId: new("5f294f92-bafb-4011-916d-9b0bexample"), + VirtualGatewayId: new("vgw-6efe725e"), + StateChangeError: new("something went wrong"), }, }, } 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 096629a3..936b5a33 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway-attachment_test.go @@ -15,10 +15,10 @@ func TestDirectConnectGatewayAttachmentOutputMapper_Health_OK(t *testing.T) { output := &directconnect.DescribeDirectConnectGatewayAttachmentsOutput{ DirectConnectGatewayAttachments: []types.DirectConnectGatewayAttachment{ { - VirtualInterfaceOwnerAccount: PtrString("123456789012"), - VirtualInterfaceRegion: PtrString("us-east-2"), - VirtualInterfaceId: PtrString("dxvif-ffhhk74f"), - DirectConnectGatewayId: PtrString("cf68415c-f4ae-48f2-87a7-3b52cexample"), + VirtualInterfaceOwnerAccount: new("123456789012"), + VirtualInterfaceRegion: new("us-east-2"), + VirtualInterfaceId: new("dxvif-ffhhk74f"), + DirectConnectGatewayId: new("cf68415c-f4ae-48f2-87a7-3b52cexample"), AttachmentState: "detaching", }, }, @@ -67,12 +67,12 @@ func TestDirectConnectGatewayAttachmentOutputMapper_Health_Error(t *testing.T) { output := &directconnect.DescribeDirectConnectGatewayAttachmentsOutput{ DirectConnectGatewayAttachments: []types.DirectConnectGatewayAttachment{ { - VirtualInterfaceOwnerAccount: PtrString("123456789012"), - VirtualInterfaceRegion: PtrString("us-east-2"), - VirtualInterfaceId: PtrString("dxvif-ffhhk74f"), - DirectConnectGatewayId: PtrString("cf68415c-f4ae-48f2-87a7-3b52cexample"), + VirtualInterfaceOwnerAccount: new("123456789012"), + VirtualInterfaceRegion: new("us-east-2"), + VirtualInterfaceId: new("dxvif-ffhhk74f"), + DirectConnectGatewayId: new("cf68415c-f4ae-48f2-87a7-3b52cexample"), AttachmentState: "detaching", - StateChangeError: PtrString("error"), + StateChangeError: new("error"), }, }, } diff --git a/aws-source/adapters/directconnect-direct-connect-gateway_test.go b/aws-source/adapters/directconnect-direct-connect-gateway_test.go index 32ed2630..db876da3 100644 --- a/aws-source/adapters/directconnect-direct-connect-gateway_test.go +++ b/aws-source/adapters/directconnect-direct-connect-gateway_test.go @@ -15,10 +15,10 @@ func TestDirectConnectGatewayOutputMapper_Health_OK(t *testing.T) { output := &directconnect.DescribeDirectConnectGatewaysOutput{ DirectConnectGateways: []types.DirectConnectGateway{ { - AmazonSideAsn: PtrInt64(64512), - DirectConnectGatewayId: PtrString("cf68415c-f4ae-48f2-87a7-3b52cexample"), - OwnerAccount: PtrString("123456789012"), - DirectConnectGatewayName: PtrString("DxGateway2"), + AmazonSideAsn: new(int64(64512)), + DirectConnectGatewayId: new("cf68415c-f4ae-48f2-87a7-3b52cexample"), + OwnerAccount: new("123456789012"), + DirectConnectGatewayName: new("DxGateway2"), DirectConnectGatewayState: types.DirectConnectGatewayStateAvailable, }, }, @@ -48,12 +48,12 @@ func TestDirectConnectGatewayOutputMapper_Health_ERROR(t *testing.T) { output := &directconnect.DescribeDirectConnectGatewaysOutput{ DirectConnectGateways: []types.DirectConnectGateway{ { - AmazonSideAsn: PtrInt64(64512), - DirectConnectGatewayId: PtrString("cf68415c-f4ae-48f2-87a7-3b52cexample"), - OwnerAccount: PtrString("123456789012"), - DirectConnectGatewayName: PtrString("DxGateway2"), + AmazonSideAsn: new(int64(64512)), + DirectConnectGatewayId: new("cf68415c-f4ae-48f2-87a7-3b52cexample"), + OwnerAccount: new("123456789012"), + DirectConnectGatewayName: new("DxGateway2"), DirectConnectGatewayState: types.DirectConnectGatewayStateAvailable, - StateChangeError: PtrString("error"), + StateChangeError: new("error"), }, }, } diff --git a/aws-source/adapters/directconnect-hosted-connection_test.go b/aws-source/adapters/directconnect-hosted-connection_test.go index 6e869b45..59b3458a 100644 --- a/aws-source/adapters/directconnect-hosted-connection_test.go +++ b/aws-source/adapters/directconnect-hosted-connection_test.go @@ -15,26 +15,26 @@ func TestHostedConnectionOutputMapper(t *testing.T) { output := &directconnect.DescribeHostedConnectionsOutput{ Connections: []types.Connection{ { - AwsDeviceV2: PtrString("EqDC2-123h49s71dabc"), - AwsLogicalDeviceId: PtrString("device-1"), - Bandwidth: PtrString("1Gbps"), - ConnectionId: PtrString("dxcon-fguhmqlc"), - ConnectionName: PtrString("My_Connection"), + AwsDeviceV2: new("EqDC2-123h49s71dabc"), + AwsLogicalDeviceId: new("device-1"), + Bandwidth: new("1Gbps"), + ConnectionId: new("dxcon-fguhmqlc"), + ConnectionName: new("My_Connection"), ConnectionState: "down", - EncryptionMode: PtrString("must_encrypt"), + EncryptionMode: new("must_encrypt"), HasLogicalRedundancy: "unknown", - JumboFrameCapable: PtrBool(true), - LagId: PtrString("dxlag-ffrz71kw"), - LoaIssueTime: PtrTime(time.Now()), - Location: PtrString("EqDC2"), - Region: PtrString("us-east-1"), - ProviderName: PtrString("provider-1"), - OwnerAccount: PtrString("123456789012"), - PartnerName: PtrString("partner-1"), + JumboFrameCapable: new(true), + LagId: new("dxlag-ffrz71kw"), + LoaIssueTime: new(time.Now()), + Location: new("EqDC2"), + Region: new("us-east-1"), + ProviderName: new("provider-1"), + OwnerAccount: new("123456789012"), + PartnerName: new("partner-1"), Tags: []types.Tag{ { - Key: PtrString("foo"), - Value: PtrString("bar"), + Key: new("foo"), + Value: new("bar"), }, }, }, diff --git a/aws-source/adapters/directconnect-interconnect_test.go b/aws-source/adapters/directconnect-interconnect_test.go index 84f09a63..de3ccbdd 100644 --- a/aws-source/adapters/directconnect-interconnect_test.go +++ b/aws-source/adapters/directconnect-interconnect_test.go @@ -15,23 +15,23 @@ func TestInterconnectOutputMapper(t *testing.T) { output := &directconnect.DescribeInterconnectsOutput{ Interconnects: []types.Interconnect{ { - AwsDeviceV2: PtrString("EqDC2-123h49s71dabc"), - AwsLogicalDeviceId: PtrString("device-1"), - Bandwidth: PtrString("1Gbps"), + AwsDeviceV2: new("EqDC2-123h49s71dabc"), + AwsLogicalDeviceId: new("device-1"), + Bandwidth: new("1Gbps"), HasLogicalRedundancy: types.HasLogicalRedundancyUnknown, - InterconnectId: PtrString("dxcon-fguhmqlc"), - InterconnectName: PtrString("interconnect-1"), + InterconnectId: new("dxcon-fguhmqlc"), + InterconnectName: new("interconnect-1"), InterconnectState: types.InterconnectStateAvailable, - JumboFrameCapable: PtrBool(true), - LagId: PtrString("dxlag-ffrz71kw"), - LoaIssueTime: PtrTime(time.Now()), - Location: PtrString("EqDC2"), - Region: PtrString("us-east-1"), - ProviderName: PtrString("provider-1"), + JumboFrameCapable: new(true), + LagId: new("dxlag-ffrz71kw"), + LoaIssueTime: new(time.Now()), + Location: new("EqDC2"), + Region: new("us-east-1"), + ProviderName: new("provider-1"), Tags: []types.Tag{ { - Key: PtrString("foo"), - Value: PtrString("bar"), + Key: new("foo"), + Value: new("bar"), }, }, }, @@ -125,7 +125,7 @@ func TestInterconnectHealth(t *testing.T) { Interconnects: []types.Interconnect{ { InterconnectState: c.state, - LagId: PtrString("dxlag-fgsu9erb"), + LagId: new("dxlag-fgsu9erb"), }, }, } diff --git a/aws-source/adapters/directconnect-lag_test.go b/aws-source/adapters/directconnect-lag_test.go index 6632482b..125968f1 100644 --- a/aws-source/adapters/directconnect-lag_test.go +++ b/aws-source/adapters/directconnect-lag_test.go @@ -52,7 +52,7 @@ func TestLagHealth(t *testing.T) { Lags: []types.Lag{ { LagState: c.state, - LagId: PtrString("dxlag-fgsu9erb"), + LagId: new("dxlag-fgsu9erb"), }, }, } @@ -78,38 +78,38 @@ func TestLagOutputMapper(t *testing.T) { output := &directconnect.DescribeLagsOutput{ Lags: []types.Lag{ { - AwsDeviceV2: PtrString("EqDC2-19y7z3m17xpuz"), + AwsDeviceV2: new("EqDC2-19y7z3m17xpuz"), NumberOfConnections: int32(2), LagState: types.LagStateAvailable, - OwnerAccount: PtrString("123456789012"), - LagName: PtrString("DA-LAG"), + OwnerAccount: new("123456789012"), + LagName: new("DA-LAG"), Connections: []types.Connection{ { - OwnerAccount: PtrString("123456789012"), - ConnectionId: PtrString("dxcon-ffnikghc"), - LagId: PtrString("dxlag-fgsu9erb"), + OwnerAccount: new("123456789012"), + ConnectionId: new("dxcon-ffnikghc"), + LagId: new("dxlag-fgsu9erb"), ConnectionState: "requested", - Bandwidth: PtrString("10Gbps"), - Location: PtrString("EqDC2"), - ConnectionName: PtrString("Requested Connection 1 for Lag dxlag-fgsu9erb"), - Region: PtrString("us-east-1"), + Bandwidth: new("10Gbps"), + Location: new("EqDC2"), + ConnectionName: new("Requested Connection 1 for Lag dxlag-fgsu9erb"), + Region: new("us-east-1"), }, { - OwnerAccount: PtrString("123456789012"), - ConnectionId: PtrString("dxcon-fglgbdea"), - LagId: PtrString("dxlag-fgsu9erb"), + OwnerAccount: new("123456789012"), + ConnectionId: new("dxcon-fglgbdea"), + LagId: new("dxlag-fgsu9erb"), ConnectionState: "requested", - Bandwidth: PtrString("10Gbps"), - Location: PtrString("EqDC2"), - ConnectionName: PtrString("Requested Connection 2 for Lag dxlag-fgsu9erb"), - Region: PtrString("us-east-1"), + Bandwidth: new("10Gbps"), + Location: new("EqDC2"), + ConnectionName: new("Requested Connection 2 for Lag dxlag-fgsu9erb"), + Region: new("us-east-1"), }, }, - LagId: PtrString("dxlag-fgsu9erb"), + LagId: new("dxlag-fgsu9erb"), MinimumLinks: int32(0), - ConnectionsBandwidth: PtrString("10Gbps"), - Region: PtrString("us-east-1"), - Location: PtrString("EqDC2"), + ConnectionsBandwidth: new("10Gbps"), + Region: new("us-east-1"), + Location: new("EqDC2"), }, }, } diff --git a/aws-source/adapters/directconnect-location_test.go b/aws-source/adapters/directconnect-location_test.go index 9c18c9f6..e84e8e3b 100644 --- a/aws-source/adapters/directconnect-location_test.go +++ b/aws-source/adapters/directconnect-location_test.go @@ -17,9 +17,9 @@ func TestLocationOutputMapper(t *testing.T) { AvailableMacSecPortSpeeds: []string{"1 Gbps", "10 Gbps"}, AvailablePortSpeeds: []string{"50 Mbps", "100 Mbps", "1 Gbps", "10 Gbps"}, AvailableProviders: []string{"ProviderA", "ProviderB", "ProviderC"}, - LocationName: PtrString("NAP do Brasil, Barueri, Sao Paulo"), - LocationCode: PtrString("TNDB"), - Region: PtrString("us-east-1"), + LocationName: new("NAP do Brasil, Barueri, Sao Paulo"), + LocationCode: new("TNDB"), + Region: new("us-east-1"), }, }, } diff --git a/aws-source/adapters/directconnect-router-configuration_test.go b/aws-source/adapters/directconnect-router-configuration_test.go index 6a151e2d..c8b4ffb9 100644 --- a/aws-source/adapters/directconnect-router-configuration_test.go +++ b/aws-source/adapters/directconnect-router-configuration_test.go @@ -13,17 +13,17 @@ import ( func TestRouterConfigurationOutputMapper(t *testing.T) { output := &directconnect.DescribeRouterConfigurationOutput{ - CustomerRouterConfig: PtrString("some config"), + CustomerRouterConfig: new("some config"), Router: &types.RouterType{ - Platform: PtrString("2900 Series Routers"), - RouterTypeIdentifier: PtrString("CiscoSystemsInc-2900SeriesRouters-IOS124"), - Software: PtrString("IOS 12.4+"), - Vendor: PtrString("Cisco Systems, Inc."), - XsltTemplateName: PtrString("customer-router-cisco-generic.xslt"), - XsltTemplateNameForMacSec: PtrString(""), + Platform: new("2900 Series Routers"), + RouterTypeIdentifier: new("CiscoSystemsInc-2900SeriesRouters-IOS124"), + Software: new("IOS 12.4+"), + Vendor: new("Cisco Systems, Inc."), + XsltTemplateName: new("customer-router-cisco-generic.xslt"), + XsltTemplateNameForMacSec: new(""), }, - VirtualInterfaceId: PtrString("dxvif-ffhhk74f"), - VirtualInterfaceName: PtrString("PrivateVirtualInterface"), + VirtualInterfaceId: new("dxvif-ffhhk74f"), + VirtualInterfaceName: new("PrivateVirtualInterface"), } items, err := routerConfigurationOutputMapper(context.Background(), nil, "foo", nil, output) diff --git a/aws-source/adapters/directconnect-virtual-gateway_test.go b/aws-source/adapters/directconnect-virtual-gateway_test.go index c4003bb5..85245770 100644 --- a/aws-source/adapters/directconnect-virtual-gateway_test.go +++ b/aws-source/adapters/directconnect-virtual-gateway_test.go @@ -14,8 +14,8 @@ func TestVirtualGatewayOutputMapper(t *testing.T) { output := &directconnect.DescribeVirtualGatewaysOutput{ VirtualGateways: []types.VirtualGateway{ { - VirtualGatewayId: PtrString("cf68415c-f4ae-48f2-87a7-3b52cexample"), - VirtualGatewayState: PtrString("available"), + VirtualGatewayId: new("cf68415c-f4ae-48f2-87a7-3b52cexample"), + VirtualGatewayState: new("available"), }, }, } diff --git a/aws-source/adapters/directconnect-virtual-interface_test.go b/aws-source/adapters/directconnect-virtual-interface_test.go index 396737b2..474489c6 100644 --- a/aws-source/adapters/directconnect-virtual-interface_test.go +++ b/aws-source/adapters/directconnect-virtual-interface_test.go @@ -17,14 +17,14 @@ func TestVirtualInterfaceOutputMapper(t *testing.T) { output := &directconnect.DescribeVirtualInterfacesOutput{ VirtualInterfaces: []types.VirtualInterface{ { - VirtualInterfaceId: PtrString("dxvif-ffhhk74f"), - ConnectionId: PtrString("dxcon-fguhmqlc"), + VirtualInterfaceId: new("dxvif-ffhhk74f"), + ConnectionId: new("dxcon-fguhmqlc"), VirtualInterfaceState: "verifying", - CustomerAddress: PtrString("192.168.1.2/30"), - AmazonAddress: PtrString("192.168.1.1/30"), - VirtualInterfaceType: PtrString("private"), - VirtualInterfaceName: PtrString("PrivateVirtualInterface"), - DirectConnectGatewayId: PtrString("cf68415c-f4ae-48f2-87a7-3b52cexample"), + CustomerAddress: new("192.168.1.2/30"), + AmazonAddress: new("192.168.1.1/30"), + VirtualInterfaceType: new("private"), + VirtualInterfaceName: new("PrivateVirtualInterface"), + DirectConnectGatewayId: new("cf68415c-f4ae-48f2-87a7-3b52cexample"), }, }, } diff --git a/aws-source/adapters/dynamodb-backup_test.go b/aws-source/adapters/dynamodb-backup_test.go index 4b924a28..6046d1f8 100644 --- a/aws-source/adapters/dynamodb-backup_test.go +++ b/aws-source/adapters/dynamodb-backup_test.go @@ -15,43 +15,43 @@ func (t *DynamoDBTestClient) DescribeBackup(ctx context.Context, params *dynamod return &dynamodb.DescribeBackupOutput{ BackupDescription: &types.BackupDescription{ BackupDetails: &types.BackupDetails{ - BackupArn: PtrString("arn:aws:dynamodb:eu-west-1:052392120703:table/test2/backup/01673461724486-a6007753"), - BackupName: PtrString("test2-backup"), - BackupSizeBytes: PtrInt64(0), + BackupArn: new("arn:aws:dynamodb:eu-west-1:052392120703:table/test2/backup/01673461724486-a6007753"), + BackupName: new("test2-backup"), + BackupSizeBytes: new(int64(0)), BackupStatus: types.BackupStatusAvailable, BackupType: types.BackupTypeUser, - BackupCreationDateTime: PtrTime(time.Now()), + BackupCreationDateTime: new(time.Now()), }, SourceTableDetails: &types.SourceTableDetails{ - TableName: PtrString("test2"), // link - TableId: PtrString("12670f3b-8ca1-463b-b15e-f2e27eaf70b0"), - TableArn: PtrString("arn:aws:dynamodb:eu-west-1:052392120703:table/test2"), - TableSizeBytes: PtrInt64(0), + TableName: new("test2"), // link + TableId: new("12670f3b-8ca1-463b-b15e-f2e27eaf70b0"), + TableArn: new("arn:aws:dynamodb:eu-west-1:052392120703:table/test2"), + TableSizeBytes: new(int64(0)), KeySchema: []types.KeySchemaElement{ { - AttributeName: PtrString("ArtistId"), + AttributeName: new("ArtistId"), KeyType: types.KeyTypeHash, }, { - AttributeName: PtrString("Concert"), + AttributeName: new("Concert"), KeyType: types.KeyTypeRange, }, }, - TableCreationDateTime: PtrTime(time.Now()), + TableCreationDateTime: new(time.Now()), ProvisionedThroughput: &types.ProvisionedThroughput{ - ReadCapacityUnits: PtrInt64(5), - WriteCapacityUnits: PtrInt64(5), + ReadCapacityUnits: new(int64(5)), + WriteCapacityUnits: new(int64(5)), }, - ItemCount: PtrInt64(0), + ItemCount: new(int64(0)), BillingMode: types.BillingModeProvisioned, }, SourceTableFeatureDetails: &types.SourceTableFeatureDetails{ GlobalSecondaryIndexes: []types.GlobalSecondaryIndexInfo{ { - IndexName: PtrString("GSI"), + IndexName: new("GSI"), KeySchema: []types.KeySchemaElement{ { - AttributeName: PtrString("TicketSales"), + AttributeName: new("TicketSales"), KeyType: types.KeyTypeHash, }, }, @@ -59,8 +59,8 @@ func (t *DynamoDBTestClient) DescribeBackup(ctx context.Context, params *dynamod ProjectionType: types.ProjectionTypeKeysOnly, }, ProvisionedThroughput: &types.ProvisionedThroughput{ - ReadCapacityUnits: PtrInt64(5), - WriteCapacityUnits: PtrInt64(5), + ReadCapacityUnits: new(int64(5)), + WriteCapacityUnits: new(int64(5)), }, }, }, @@ -73,15 +73,15 @@ func (t *DynamoDBTestClient) ListBackups(ctx context.Context, params *dynamodb.L return &dynamodb.ListBackupsOutput{ BackupSummaries: []types.BackupSummary{ { - TableName: PtrString("test2"), - TableId: PtrString("12670f3b-8ca1-463b-b15e-f2e27eaf70b0"), - TableArn: PtrString("arn:aws:dynamodb:eu-west-1:052392120703:table/test2"), - BackupArn: PtrString("arn:aws:dynamodb:eu-west-1:052392120703:table/test2/backup/01673461724486-a6007753"), - BackupName: PtrString("test2-backup"), - BackupCreationDateTime: PtrTime(time.Now()), + TableName: new("test2"), + TableId: new("12670f3b-8ca1-463b-b15e-f2e27eaf70b0"), + TableArn: new("arn:aws:dynamodb:eu-west-1:052392120703:table/test2"), + BackupArn: new("arn:aws:dynamodb:eu-west-1:052392120703:table/test2/backup/01673461724486-a6007753"), + BackupName: new("test2-backup"), + BackupCreationDateTime: new(time.Now()), BackupStatus: types.BackupStatusAvailable, BackupType: types.BackupTypeUser, - BackupSizeBytes: PtrInt64(10), + BackupSizeBytes: new(int64(10)), }, }, }, nil diff --git a/aws-source/adapters/dynamodb-table_test.go b/aws-source/adapters/dynamodb-table_test.go index ecbbfadd..ac7924d9 100644 --- a/aws-source/adapters/dynamodb-table_test.go +++ b/aws-source/adapters/dynamodb-table_test.go @@ -16,46 +16,46 @@ func (t *DynamoDBTestClient) DescribeTable(context.Context, *dynamodb.DescribeTa Table: &types.TableDescription{ AttributeDefinitions: []types.AttributeDefinition{ { - AttributeName: PtrString("ArtistId"), + AttributeName: new("ArtistId"), AttributeType: types.ScalarAttributeTypeS, }, { - AttributeName: PtrString("Concert"), + AttributeName: new("Concert"), AttributeType: types.ScalarAttributeTypeS, }, { - AttributeName: PtrString("TicketSales"), + AttributeName: new("TicketSales"), AttributeType: types.ScalarAttributeTypeS, }, }, - TableName: PtrString("test-DDBTable-1X52D7BWAAB2H"), + TableName: new("test-DDBTable-1X52D7BWAAB2H"), KeySchema: []types.KeySchemaElement{ { - AttributeName: PtrString("ArtistId"), + AttributeName: new("ArtistId"), KeyType: types.KeyTypeHash, }, { - AttributeName: PtrString("Concert"), + AttributeName: new("Concert"), KeyType: types.KeyTypeRange, }, }, TableStatus: types.TableStatusActive, - CreationDateTime: PtrTime(time.Now()), + CreationDateTime: new(time.Now()), ProvisionedThroughput: &types.ProvisionedThroughputDescription{ - NumberOfDecreasesToday: PtrInt64(0), - ReadCapacityUnits: PtrInt64(5), - WriteCapacityUnits: PtrInt64(5), + NumberOfDecreasesToday: new(int64(0)), + ReadCapacityUnits: new(int64(5)), + WriteCapacityUnits: new(int64(5)), }, - TableSizeBytes: PtrInt64(0), - ItemCount: PtrInt64(0), - TableArn: PtrString("arn:aws:dynamodb:eu-west-1:052392120703:table/test-DDBTable-1X52D7BWAAB2H"), - TableId: PtrString("32ef65bf-d6f3-4508-a3db-f201df09e437"), + TableSizeBytes: new(int64(0)), + ItemCount: new(int64(0)), + TableArn: new("arn:aws:dynamodb:eu-west-1:052392120703:table/test-DDBTable-1X52D7BWAAB2H"), + TableId: new("32ef65bf-d6f3-4508-a3db-f201df09e437"), GlobalSecondaryIndexes: []types.GlobalSecondaryIndexDescription{ { - IndexName: PtrString("GSI"), + IndexName: new("GSI"), KeySchema: []types.KeySchemaElement{ { - AttributeName: PtrString("TicketSales"), + AttributeName: new("TicketSales"), KeyType: types.KeyTypeHash, }, }, @@ -64,35 +64,35 @@ func (t *DynamoDBTestClient) DescribeTable(context.Context, *dynamodb.DescribeTa }, IndexStatus: types.IndexStatusActive, ProvisionedThroughput: &types.ProvisionedThroughputDescription{ - NumberOfDecreasesToday: PtrInt64(0), - ReadCapacityUnits: PtrInt64(5), - WriteCapacityUnits: PtrInt64(5), + NumberOfDecreasesToday: new(int64(0)), + ReadCapacityUnits: new(int64(5)), + WriteCapacityUnits: new(int64(5)), }, - IndexSizeBytes: PtrInt64(0), - ItemCount: PtrInt64(0), - IndexArn: PtrString("arn:aws:dynamodb:eu-west-1:052392120703:table/test-DDBTable-1X52D7BWAAB2H/index/GSI"), // no link, t + IndexSizeBytes: new(int64(0)), + ItemCount: new(int64(0)), + IndexArn: new("arn:aws:dynamodb:eu-west-1:052392120703:table/test-DDBTable-1X52D7BWAAB2H/index/GSI"), // no link, t }, }, ArchivalSummary: &types.ArchivalSummary{ - ArchivalBackupArn: PtrString("arn:aws:backups:eu-west-1:052392120703:some-backup/one"), // link - ArchivalDateTime: PtrTime(time.Now()), - ArchivalReason: PtrString("fear"), + ArchivalBackupArn: new("arn:aws:backups:eu-west-1:052392120703:some-backup/one"), // link + ArchivalDateTime: new(time.Now()), + ArchivalReason: new("fear"), }, BillingModeSummary: &types.BillingModeSummary{ BillingMode: types.BillingModePayPerRequest, }, - GlobalTableVersion: PtrString("1"), - LatestStreamArn: PtrString("arn:aws:dynamodb:eu-west-1:052392120703:table/test-DDBTable-1X52D7BWAAB2H/stream/2023-01-11T16:53:02.371"), // This doesn't get linked because there is no more data to get - LatestStreamLabel: PtrString("2023-01-11T16:53:02.371"), + GlobalTableVersion: new("1"), + LatestStreamArn: new("arn:aws:dynamodb:eu-west-1:052392120703:table/test-DDBTable-1X52D7BWAAB2H/stream/2023-01-11T16:53:02.371"), // This doesn't get linked because there is no more data to get + LatestStreamLabel: new("2023-01-11T16:53:02.371"), LocalSecondaryIndexes: []types.LocalSecondaryIndexDescription{ { - IndexArn: PtrString("arn:aws:dynamodb:eu-west-1:052392120703:table/test-DDBTable-1X52D7BWAAB2H/index/GSX"), // no link - IndexName: PtrString("GSX"), - IndexSizeBytes: PtrInt64(29103), - ItemCount: PtrInt64(234234), + IndexArn: new("arn:aws:dynamodb:eu-west-1:052392120703:table/test-DDBTable-1X52D7BWAAB2H/index/GSX"), // no link + IndexName: new("GSX"), + IndexSizeBytes: new(int64(29103)), + ItemCount: new(int64(234234)), KeySchema: []types.KeySchemaElement{ { - AttributeName: PtrString("TicketSales"), + AttributeName: new("TicketSales"), KeyType: types.KeyTypeHash, }, }, @@ -108,11 +108,11 @@ func (t *DynamoDBTestClient) DescribeTable(context.Context, *dynamodb.DescribeTa { GlobalSecondaryIndexes: []types.ReplicaGlobalSecondaryIndexDescription{ { - IndexName: PtrString("name"), + IndexName: new("name"), }, }, - KMSMasterKeyId: PtrString("keyID"), - RegionName: PtrString("eu-west-2"), // link + KMSMasterKeyId: new("keyID"), + RegionName: new("eu-west-2"), // link ReplicaStatus: types.ReplicaStatusActive, ReplicaTableClassSummary: &types.TableClassSummary{ TableClass: types.TableClassStandard, @@ -120,23 +120,23 @@ func (t *DynamoDBTestClient) DescribeTable(context.Context, *dynamodb.DescribeTa }, }, RestoreSummary: &types.RestoreSummary{ - RestoreDateTime: PtrTime(time.Now()), - RestoreInProgress: PtrBool(false), - SourceBackupArn: PtrString("arn:aws:backup:eu-west-1:052392120703:recovery-point:89d0f956-d3a6-42fd-abbd-7d397766bc7e"), // link - SourceTableArn: PtrString("arn:aws:dynamodb:eu-west-1:052392120703:table/test-DDBTable-1X52D7BWAAB2H"), // link + RestoreDateTime: new(time.Now()), + RestoreInProgress: new(false), + SourceBackupArn: new("arn:aws:backup:eu-west-1:052392120703:recovery-point:89d0f956-d3a6-42fd-abbd-7d397766bc7e"), // link + SourceTableArn: new("arn:aws:dynamodb:eu-west-1:052392120703:table/test-DDBTable-1X52D7BWAAB2H"), // link }, SSEDescription: &types.SSEDescription{ - InaccessibleEncryptionDateTime: PtrTime(time.Now()), - KMSMasterKeyArn: PtrString("arn:aws:service:region:account:type/id"), // link + InaccessibleEncryptionDateTime: new(time.Now()), + KMSMasterKeyArn: new("arn:aws:service:region:account:type/id"), // link SSEType: types.SSETypeAes256, Status: types.SSEStatusDisabling, }, StreamSpecification: &types.StreamSpecification{ - StreamEnabled: PtrBool(true), + StreamEnabled: new(true), StreamViewType: types.StreamViewTypeKeysOnly, }, TableClassSummary: &types.TableClassSummary{ - LastUpdateDateTime: PtrTime(time.Now()), + LastUpdateDateTime: new(time.Now()), TableClass: types.TableClassStandard, }, }, @@ -156,8 +156,8 @@ func (t *DynamoDBTestClient) DescribeKinesisStreamingDestination(ctx context.Con KinesisDataStreamDestinations: []types.KinesisDataStreamDestination{ { DestinationStatus: types.DestinationStatusActive, - DestinationStatusDescription: PtrString("description"), - StreamArn: PtrString("arn:aws:kinesis:eu-west-1:052392120703:stream/test"), + DestinationStatusDescription: new("description"), + StreamArn: new("arn:aws:kinesis:eu-west-1:052392120703:stream/test"), }, }, }, nil @@ -167,8 +167,8 @@ func (t *DynamoDBTestClient) ListTagsOfResource(context.Context, *dynamodb.ListT return &dynamodb.ListTagsOfResourceOutput{ Tags: []types.Tag{ { - Key: PtrString("key"), - Value: PtrString("value"), + Key: new("key"), + Value: new("value"), }, }, NextToken: nil, diff --git a/aws-source/adapters/ec2-address_test.go b/aws-source/adapters/ec2-address_test.go index 59262cc9..41a3bf7e 100644 --- a/aws-source/adapters/ec2-address_test.go +++ b/aws-source/adapters/ec2-address_test.go @@ -43,16 +43,16 @@ func TestAddressOutputMapper(t *testing.T) { output := ec2.DescribeAddressesOutput{ Addresses: []types.Address{ { - PublicIp: PtrString("3.11.82.6"), - AllocationId: PtrString("eipalloc-030a6f43bc6086267"), + PublicIp: new("3.11.82.6"), + AllocationId: new("eipalloc-030a6f43bc6086267"), Domain: types.DomainTypeVpc, - PublicIpv4Pool: PtrString("amazon"), - NetworkBorderGroup: PtrString("eu-west-2"), - InstanceId: PtrString("instance"), - CarrierIp: PtrString("3.11.82.7"), - CustomerOwnedIp: PtrString("3.11.82.8"), - NetworkInterfaceId: PtrString("foo"), - PrivateIpAddress: PtrString("3.11.82.9"), + PublicIpv4Pool: new("amazon"), + NetworkBorderGroup: new("eu-west-2"), + InstanceId: new("instance"), + CarrierIp: new("3.11.82.7"), + CustomerOwnedIp: new("3.11.82.8"), + NetworkInterfaceId: new("foo"), + PrivateIpAddress: new("3.11.82.9"), }, }, } diff --git a/aws-source/adapters/ec2-capacity-reservation-fleet_test.go b/aws-source/adapters/ec2-capacity-reservation-fleet_test.go index cb81a86f..358fac63 100644 --- a/aws-source/adapters/ec2-capacity-reservation-fleet_test.go +++ b/aws-source/adapters/ec2-capacity-reservation-fleet_test.go @@ -14,31 +14,31 @@ func TestCapacityReservationFleetOutputMapper(t *testing.T) { output := &ec2.DescribeCapacityReservationFleetsOutput{ CapacityReservationFleets: []types.CapacityReservationFleet{ { - AllocationStrategy: PtrString("prioritized"), - CapacityReservationFleetArn: PtrString("arn:aws:ec2:us-east-1:123456789012:capacity-reservation/fleet/crf-1234567890abcdef0"), - CapacityReservationFleetId: PtrString("crf-1234567890abcdef0"), - CreateTime: PtrTime(time.Now()), + AllocationStrategy: new("prioritized"), + CapacityReservationFleetArn: new("arn:aws:ec2:us-east-1:123456789012:capacity-reservation/fleet/crf-1234567890abcdef0"), + CapacityReservationFleetId: new("crf-1234567890abcdef0"), + CreateTime: new(time.Now()), EndDate: nil, InstanceMatchCriteria: types.FleetInstanceMatchCriteriaOpen, InstanceTypeSpecifications: []types.FleetCapacityReservation{ { - AvailabilityZone: PtrString("us-east-1a"), // link - AvailabilityZoneId: PtrString("use1-az1"), - CapacityReservationId: PtrString("cr-1234567890abcdef0"), // link - CreateDate: PtrTime(time.Now()), - EbsOptimized: PtrBool(true), - FulfilledCapacity: PtrFloat64(1), + AvailabilityZone: new("us-east-1a"), // link + AvailabilityZoneId: new("use1-az1"), + CapacityReservationId: new("cr-1234567890abcdef0"), // link + CreateDate: new(time.Now()), + EbsOptimized: new(true), + FulfilledCapacity: new(float64(1)), InstancePlatform: types.CapacityReservationInstancePlatformLinuxUnix, InstanceType: types.InstanceTypeA12xlarge, - Priority: PtrInt32(1), - TotalInstanceCount: PtrInt32(1), - Weight: PtrFloat64(1), + Priority: new(int32(1)), + TotalInstanceCount: new(int32(1)), + Weight: new(float64(1)), }, }, State: types.CapacityReservationFleetStateActive, // health Tenancy: types.FleetCapacityReservationTenancyDefault, - TotalFulfilledCapacity: PtrFloat64(1), - TotalTargetCapacity: PtrInt32(1), + TotalFulfilledCapacity: new(float64(1)), + TotalTargetCapacity: new(int32(1)), }, }, } diff --git a/aws-source/adapters/ec2-capacity-reservation_test.go b/aws-source/adapters/ec2-capacity-reservation_test.go index 94394fec..0f165949 100644 --- a/aws-source/adapters/ec2-capacity-reservation_test.go +++ b/aws-source/adapters/ec2-capacity-reservation_test.go @@ -15,30 +15,30 @@ func TestCapacityReservationOutputMapper(t *testing.T) { output := &ec2.DescribeCapacityReservationsOutput{ CapacityReservations: []types.CapacityReservation{ { - AvailabilityZone: PtrString("us-east-1a"), // links - AvailabilityZoneId: PtrString("use1-az1"), - AvailableInstanceCount: PtrInt32(1), - CapacityReservationArn: PtrString("arn:aws:ec2:us-east-1:123456789012:capacity-reservation/cr-1234567890abcdef0"), - CapacityReservationId: PtrString("cr-1234567890abcdef0"), - CapacityReservationFleetId: PtrString("crf-1234567890abcdef0"), // link - CreateDate: PtrTime(time.Now()), - EbsOptimized: PtrBool(true), + AvailabilityZone: new("us-east-1a"), // links + AvailabilityZoneId: new("use1-az1"), + AvailableInstanceCount: new(int32(1)), + CapacityReservationArn: new("arn:aws:ec2:us-east-1:123456789012:capacity-reservation/cr-1234567890abcdef0"), + CapacityReservationId: new("cr-1234567890abcdef0"), + CapacityReservationFleetId: new("crf-1234567890abcdef0"), // link + CreateDate: new(time.Now()), + EbsOptimized: new(true), EndDateType: types.EndDateTypeUnlimited, EndDate: nil, InstanceMatchCriteria: types.InstanceMatchCriteriaTargeted, InstancePlatform: types.CapacityReservationInstancePlatformLinuxUnix, - InstanceType: PtrString("t2.micro"), - OutpostArn: PtrString("arn:aws:ec2:us-east-1:123456789012:outpost/op-1234567890abcdef0"), // link - OwnerId: PtrString("123456789012"), - PlacementGroupArn: PtrString("arn:aws:ec2:us-east-1:123456789012:placement-group/pg-1234567890abcdef0"), // link - StartDate: PtrTime(time.Now()), + InstanceType: new("t2.micro"), + OutpostArn: new("arn:aws:ec2:us-east-1:123456789012:outpost/op-1234567890abcdef0"), // link + OwnerId: new("123456789012"), + PlacementGroupArn: new("arn:aws:ec2:us-east-1:123456789012:placement-group/pg-1234567890abcdef0"), // link + StartDate: new(time.Now()), State: types.CapacityReservationStateActive, Tenancy: types.CapacityReservationTenancyDefault, - TotalInstanceCount: PtrInt32(1), + TotalInstanceCount: new(int32(1)), CapacityAllocations: []types.CapacityAllocation{ { AllocationType: types.AllocationTypeUsed, - Count: PtrInt32(1), + Count: new(int32(1)), }, }, }, 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 4f1362d0..1bcd11fe 100644 --- a/aws-source/adapters/ec2-egress-only-internet-gateway_test.go +++ b/aws-source/adapters/ec2-egress-only-internet-gateway_test.go @@ -46,10 +46,10 @@ func TestEgressOnlyInternetGatewayOutputMapper(t *testing.T) { Attachments: []types.InternetGatewayAttachment{ { State: types.AttachmentStatusAttached, - VpcId: PtrString("vpc-0d7892e00e573e701"), + VpcId: new("vpc-0d7892e00e573e701"), }, }, - EgressOnlyInternetGatewayId: PtrString("eigw-0ff50f360e066777a"), + EgressOnlyInternetGatewayId: new("eigw-0ff50f360e066777a"), }, }, } 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 a0573570..e60b665a 100644 --- a/aws-source/adapters/ec2-iam-instance-profile-association_test.go +++ b/aws-source/adapters/ec2-iam-instance-profile-association_test.go @@ -15,14 +15,14 @@ func TestIamInstanceProfileAssociationOutputMapper(t *testing.T) { output := ec2.DescribeIamInstanceProfileAssociationsOutput{ IamInstanceProfileAssociations: []types.IamInstanceProfileAssociation{ { - AssociationId: PtrString("eipassoc-1234567890abcdef0"), + AssociationId: new("eipassoc-1234567890abcdef0"), IamInstanceProfile: &types.IamInstanceProfile{ - Arn: PtrString("arn:aws:iam::123456789012:instance-profile/webserver"), // link - Id: PtrString("AIDACKCEVSQ6C2EXAMPLE"), + Arn: new("arn:aws:iam::123456789012:instance-profile/webserver"), // link + Id: new("AIDACKCEVSQ6C2EXAMPLE"), }, - InstanceId: PtrString("i-1234567890abcdef0"), // link + InstanceId: new("i-1234567890abcdef0"), // link State: types.IamInstanceProfileAssociationStateAssociated, - Timestamp: PtrTime(time.Now()), + Timestamp: new(time.Now()), }, }, } diff --git a/aws-source/adapters/ec2-image_test.go b/aws-source/adapters/ec2-image_test.go index eadd9d08..ebdb9e58 100644 --- a/aws-source/adapters/ec2-image_test.go +++ b/aws-source/adapters/ec2-image_test.go @@ -44,38 +44,38 @@ func TestImageOutputMapper(t *testing.T) { Images: []types.Image{ { Architecture: "x86_64", - CreationDate: PtrString("2022-12-16T19:37:36.000Z"), - ImageId: PtrString("ami-0ed3646be6ecd97c5"), - ImageLocation: PtrString("052392120703/test"), + CreationDate: new("2022-12-16T19:37:36.000Z"), + ImageId: new("ami-0ed3646be6ecd97c5"), + ImageLocation: new("052392120703/test"), ImageType: types.ImageTypeValuesMachine, - Public: PtrBool(false), - OwnerId: PtrString("052392120703"), - PlatformDetails: PtrString("Linux/UNIX"), - UsageOperation: PtrString("RunInstances"), + Public: new(false), + OwnerId: new("052392120703"), + PlatformDetails: new("Linux/UNIX"), + UsageOperation: new("RunInstances"), State: types.ImageStateAvailable, BlockDeviceMappings: []types.BlockDeviceMapping{ { - DeviceName: PtrString("/dev/xvda"), + DeviceName: new("/dev/xvda"), Ebs: &types.EbsBlockDevice{ - DeleteOnTermination: PtrBool(true), - SnapshotId: PtrString("snap-0efd796ecbd599f8d"), - VolumeSize: PtrInt32(8), + DeleteOnTermination: new(true), + SnapshotId: new("snap-0efd796ecbd599f8d"), + VolumeSize: new(int32(8)), VolumeType: types.VolumeTypeGp2, - Encrypted: PtrBool(false), + Encrypted: new(false), }, }, }, - EnaSupport: PtrBool(true), + EnaSupport: new(true), Hypervisor: types.HypervisorTypeXen, - Name: PtrString("test"), - RootDeviceName: PtrString("/dev/xvda"), + Name: new("test"), + RootDeviceName: new("/dev/xvda"), RootDeviceType: types.DeviceTypeEbs, - SriovNetSupport: PtrString("simple"), + SriovNetSupport: new("simple"), VirtualizationType: types.VirtualizationTypeHvm, Tags: []types.Tag{ { - Key: PtrString("Name"), - Value: PtrString("test"), + Key: new("Name"), + Value: new("test"), }, }, }, diff --git a/aws-source/adapters/ec2-instance-event-window_test.go b/aws-source/adapters/ec2-instance-event-window_test.go index e0826d43..3a2eb46d 100644 --- a/aws-source/adapters/ec2-instance-event-window_test.go +++ b/aws-source/adapters/ec2-instance-event-window_test.go @@ -51,14 +51,14 @@ func TestInstanceEventWindowOutputMapper(t *testing.T) { "instance", }, }, - CronExpression: PtrString("something"), - InstanceEventWindowId: PtrString("window-123"), - Name: PtrString("test"), + CronExpression: new("something"), + InstanceEventWindowId: new("window-123"), + Name: new("test"), State: types.InstanceEventWindowStateActive, TimeRanges: []types.InstanceEventWindowTimeRange{ { - StartHour: PtrInt32(1), - EndHour: PtrInt32(2), + StartHour: new(int32(1)), + EndHour: new(int32(2)), EndWeekDay: types.WeekDayFriday, StartWeekDay: types.WeekDayMonday, }, diff --git a/aws-source/adapters/ec2-instance-status_test.go b/aws-source/adapters/ec2-instance-status_test.go index f4ff907e..b593b8ed 100644 --- a/aws-source/adapters/ec2-instance-status_test.go +++ b/aws-source/adapters/ec2-instance-status_test.go @@ -43,10 +43,10 @@ func TestInstanceStatusOutputMapper(t *testing.T) { output := &ec2.DescribeInstanceStatusOutput{ InstanceStatuses: []types.InstanceStatus{ { - AvailabilityZone: PtrString("eu-west-2c"), // link - InstanceId: PtrString("i-022bdccde30270570"), // link + AvailabilityZone: new("eu-west-2c"), // link + InstanceId: new("i-022bdccde30270570"), // link InstanceState: &types.InstanceState{ - Code: PtrInt32(16), + Code: new(int32(16)), Name: types.InstanceStateNameRunning, }, InstanceStatus: &types.InstanceStatusSummary{ diff --git a/aws-source/adapters/ec2-instance_test.go b/aws-source/adapters/ec2-instance_test.go index 6fc7f5e9..601ed302 100644 --- a/aws-source/adapters/ec2-instance_test.go +++ b/aws-source/adapters/ec2-instance_test.go @@ -45,171 +45,171 @@ func TestInstanceOutputMapper(t *testing.T) { { Instances: []types.Instance{ { - AmiLaunchIndex: PtrInt32(0), - PublicIpAddress: PtrString("43.5.36.7"), - ImageId: PtrString("ami-04706e771f950937f"), - InstanceId: PtrString("i-04c7b2794f7bc3d6a"), + AmiLaunchIndex: new(int32(0)), + PublicIpAddress: new("43.5.36.7"), + ImageId: new("ami-04706e771f950937f"), + InstanceId: new("i-04c7b2794f7bc3d6a"), IamInstanceProfile: &types.IamInstanceProfile{ - Arn: PtrString("arn:aws:iam::052392120703:instance-profile/test"), - Id: PtrString("AIDAJQEAZVQ7Y2EYQ2Z6Q"), + Arn: new("arn:aws:iam::052392120703:instance-profile/test"), + Id: new("AIDAJQEAZVQ7Y2EYQ2Z6Q"), }, BootMode: types.BootModeValuesLegacyBios, CurrentInstanceBootMode: types.InstanceBootModeValuesLegacyBios, ElasticGpuAssociations: []types.ElasticGpuAssociation{ { - ElasticGpuAssociationId: PtrString("ega-0a1b2c3d4e5f6g7h8"), - ElasticGpuAssociationState: PtrString("associated"), - ElasticGpuAssociationTime: PtrString("now"), - ElasticGpuId: PtrString("egp-0a1b2c3d4e5f6g7h8"), + ElasticGpuAssociationId: new("ega-0a1b2c3d4e5f6g7h8"), + ElasticGpuAssociationState: new("associated"), + ElasticGpuAssociationTime: new("now"), + ElasticGpuId: new("egp-0a1b2c3d4e5f6g7h8"), }, }, - CapacityReservationId: PtrString("cr-0a1b2c3d4e5f6g7h8"), + CapacityReservationId: new("cr-0a1b2c3d4e5f6g7h8"), InstanceType: types.InstanceTypeT2Micro, ElasticInferenceAcceleratorAssociations: []types.ElasticInferenceAcceleratorAssociation{ { - ElasticInferenceAcceleratorArn: PtrString("arn:aws:elastic-inference:us-east-1:052392120703:accelerator/eia-0a1b2c3d4e5f6g7h8"), - ElasticInferenceAcceleratorAssociationId: PtrString("eiaa-0a1b2c3d4e5f6g7h8"), - ElasticInferenceAcceleratorAssociationState: PtrString("associated"), - ElasticInferenceAcceleratorAssociationTime: PtrTime(time.Now()), + ElasticInferenceAcceleratorArn: new("arn:aws:elastic-inference:us-east-1:052392120703:accelerator/eia-0a1b2c3d4e5f6g7h8"), + ElasticInferenceAcceleratorAssociationId: new("eiaa-0a1b2c3d4e5f6g7h8"), + ElasticInferenceAcceleratorAssociationState: new("associated"), + ElasticInferenceAcceleratorAssociationTime: new(time.Now()), }, }, InstanceLifecycle: types.InstanceLifecycleTypeScheduled, - Ipv6Address: PtrString("2001:db8:3333:4444:5555:6666:7777:8888"), - KeyName: PtrString("dylan.ratcliffe"), - KernelId: PtrString("aki-0a1b2c3d4e5f6g7h8"), + Ipv6Address: new("2001:db8:3333:4444:5555:6666:7777:8888"), + KeyName: new("dylan.ratcliffe"), + KernelId: new("aki-0a1b2c3d4e5f6g7h8"), Licenses: []types.LicenseConfiguration{ { - LicenseConfigurationArn: PtrString("arn:aws:license-manager:us-east-1:052392120703:license-configuration:lic-0a1b2c3d4e5f6g7h8"), + LicenseConfigurationArn: new("arn:aws:license-manager:us-east-1:052392120703:license-configuration:lic-0a1b2c3d4e5f6g7h8"), }, }, - OutpostArn: PtrString("arn:aws:outposts:us-east-1:052392120703:outpost/op-0a1b2c3d4e5f6g7h8"), + OutpostArn: new("arn:aws:outposts:us-east-1:052392120703:outpost/op-0a1b2c3d4e5f6g7h8"), Platform: types.PlatformValuesWindows, - RamdiskId: PtrString("ari-0a1b2c3d4e5f6g7h8"), - SpotInstanceRequestId: PtrString("sir-0a1b2c3d4e5f6g7h8"), - SriovNetSupport: PtrString("simple"), + RamdiskId: new("ari-0a1b2c3d4e5f6g7h8"), + SpotInstanceRequestId: new("sir-0a1b2c3d4e5f6g7h8"), + SriovNetSupport: new("simple"), StateReason: &types.StateReason{ - Code: PtrString("foo"), - Message: PtrString("bar"), + Code: new("foo"), + Message: new("bar"), }, - TpmSupport: PtrString("foo"), - LaunchTime: PtrTime(time.Now()), + TpmSupport: new("foo"), + LaunchTime: new(time.Now()), Monitoring: &types.Monitoring{ State: types.MonitoringStateDisabled, }, Placement: &types.Placement{ - AvailabilityZone: PtrString("eu-west-2c"), // link - GroupName: PtrString(""), - GroupId: PtrString("groupId"), + AvailabilityZone: new("eu-west-2c"), // link + GroupName: new(""), + GroupId: new("groupId"), Tenancy: types.TenancyDefault, }, - PrivateDnsName: PtrString("ip-172-31-95-79.eu-west-2.compute.internal"), - PrivateIpAddress: PtrString("172.31.95.79"), + PrivateDnsName: new("ip-172-31-95-79.eu-west-2.compute.internal"), + PrivateIpAddress: new("172.31.95.79"), ProductCodes: []types.ProductCode{}, - PublicDnsName: PtrString(""), + PublicDnsName: new(""), State: &types.InstanceState{ - Code: PtrInt32(16), + Code: new(int32(16)), Name: types.InstanceStateNameRunning, }, - StateTransitionReason: PtrString(""), - SubnetId: PtrString("subnet-0450a637af9984235"), - VpcId: PtrString("vpc-0d7892e00e573e701"), + StateTransitionReason: new(""), + SubnetId: new("subnet-0450a637af9984235"), + VpcId: new("vpc-0d7892e00e573e701"), Architecture: types.ArchitectureValuesX8664, BlockDeviceMappings: []types.InstanceBlockDeviceMapping{ { - DeviceName: PtrString("/dev/xvda"), + DeviceName: new("/dev/xvda"), Ebs: &types.EbsInstanceBlockDevice{ - AttachTime: PtrTime(time.Now()), - DeleteOnTermination: PtrBool(true), + AttachTime: new(time.Now()), + DeleteOnTermination: new(true), Status: types.AttachmentStatusAttached, - VolumeId: PtrString("vol-06c7211d9e79a355e"), + VolumeId: new("vol-06c7211d9e79a355e"), }, }, }, - ClientToken: PtrString("eafad400-29e0-4b5c-a0fc-ef74c77659c4"), - EbsOptimized: PtrBool(false), - EnaSupport: PtrBool(true), + ClientToken: new("eafad400-29e0-4b5c-a0fc-ef74c77659c4"), + EbsOptimized: new(false), + EnaSupport: new(true), Hypervisor: types.HypervisorTypeXen, NetworkInterfaces: []types.InstanceNetworkInterface{ { Attachment: &types.InstanceNetworkInterfaceAttachment{ - AttachTime: PtrTime(time.Now()), - AttachmentId: PtrString("eni-attach-02b19215d0dd9c7be"), - DeleteOnTermination: PtrBool(true), - DeviceIndex: PtrInt32(0), + AttachTime: new(time.Now()), + AttachmentId: new("eni-attach-02b19215d0dd9c7be"), + DeleteOnTermination: new(true), + DeviceIndex: new(int32(0)), Status: types.AttachmentStatusAttached, - NetworkCardIndex: PtrInt32(0), + NetworkCardIndex: new(int32(0)), }, - Description: PtrString(""), + Description: new(""), Groups: []types.GroupIdentifier{ { - GroupName: PtrString("default"), - GroupId: PtrString("sg-094e151c9fc5da181"), + GroupName: new("default"), + GroupId: new("sg-094e151c9fc5da181"), }, }, Ipv6Addresses: []types.InstanceIpv6Address{}, - MacAddress: PtrString("02:8c:61:38:6f:c2"), - NetworkInterfaceId: PtrString("eni-09711a69e6d511358"), - OwnerId: PtrString("052392120703"), - PrivateDnsName: PtrString("ip-172-31-95-79.eu-west-2.compute.internal"), - PrivateIpAddress: PtrString("172.31.95.79"), + MacAddress: new("02:8c:61:38:6f:c2"), + NetworkInterfaceId: new("eni-09711a69e6d511358"), + OwnerId: new("052392120703"), + PrivateDnsName: new("ip-172-31-95-79.eu-west-2.compute.internal"), + PrivateIpAddress: new("172.31.95.79"), PrivateIpAddresses: []types.InstancePrivateIpAddress{ { - Primary: PtrBool(true), - PrivateDnsName: PtrString("ip-172-31-95-79.eu-west-2.compute.internal"), - PrivateIpAddress: PtrString("172.31.95.79"), + Primary: new(true), + PrivateDnsName: new("ip-172-31-95-79.eu-west-2.compute.internal"), + PrivateIpAddress: new("172.31.95.79"), }, }, - SourceDestCheck: PtrBool(true), + SourceDestCheck: new(true), Status: types.NetworkInterfaceStatusInUse, - SubnetId: PtrString("subnet-0450a637af9984235"), - VpcId: PtrString("vpc-0d7892e00e573e701"), - InterfaceType: PtrString("interface"), + SubnetId: new("subnet-0450a637af9984235"), + VpcId: new("vpc-0d7892e00e573e701"), + InterfaceType: new("interface"), }, }, - RootDeviceName: PtrString("/dev/xvda"), + RootDeviceName: new("/dev/xvda"), RootDeviceType: types.DeviceTypeEbs, SecurityGroups: []types.GroupIdentifier{ { - GroupName: PtrString("default"), - GroupId: PtrString("sg-094e151c9fc5da181"), + GroupName: new("default"), + GroupId: new("sg-094e151c9fc5da181"), }, }, - SourceDestCheck: PtrBool(true), + SourceDestCheck: new(true), Tags: []types.Tag{ { - Key: PtrString("Name"), - Value: PtrString("test"), + Key: new("Name"), + Value: new("test"), }, }, VirtualizationType: types.VirtualizationTypeHvm, CpuOptions: &types.CpuOptions{ - CoreCount: PtrInt32(1), - ThreadsPerCore: PtrInt32(1), + CoreCount: new(int32(1)), + ThreadsPerCore: new(int32(1)), }, CapacityReservationSpecification: &types.CapacityReservationSpecificationResponse{ CapacityReservationPreference: types.CapacityReservationPreferenceOpen, }, HibernationOptions: &types.HibernationOptions{ - Configured: PtrBool(false), + Configured: new(false), }, MetadataOptions: &types.InstanceMetadataOptionsResponse{ State: types.InstanceMetadataOptionsStateApplied, HttpTokens: types.HttpTokensStateOptional, - HttpPutResponseHopLimit: PtrInt32(1), + HttpPutResponseHopLimit: new(int32(1)), HttpEndpoint: types.InstanceMetadataEndpointStateEnabled, HttpProtocolIpv6: types.InstanceMetadataProtocolStateDisabled, InstanceMetadataTags: types.InstanceMetadataTagsStateDisabled, }, EnclaveOptions: &types.EnclaveOptions{ - Enabled: PtrBool(false), + Enabled: new(false), }, - PlatformDetails: PtrString("Linux/UNIX"), - UsageOperation: PtrString("RunInstances"), - UsageOperationUpdateTime: PtrTime(time.Now()), + PlatformDetails: new("Linux/UNIX"), + UsageOperation: new("RunInstances"), + UsageOperationUpdateTime: new(time.Now()), PrivateDnsNameOptions: &types.PrivateDnsNameOptionsResponse{ HostnameType: types.HostnameTypeIpName, - EnableResourceNameDnsARecord: PtrBool(true), - EnableResourceNameDnsAAAARecord: PtrBool(false), + EnableResourceNameDnsARecord: new(true), + EnableResourceNameDnsAAAARecord: new(false), }, MaintenanceOptions: &types.InstanceMaintenanceOptions{ AutoRecovery: types.InstanceAutoRecoveryStateDefault, diff --git a/aws-source/adapters/ec2-internet-gateway_test.go b/aws-source/adapters/ec2-internet-gateway_test.go index 4c6888d5..a0b8213b 100644 --- a/aws-source/adapters/ec2-internet-gateway_test.go +++ b/aws-source/adapters/ec2-internet-gateway_test.go @@ -46,15 +46,15 @@ func TestInternetGatewayOutputMapper(t *testing.T) { Attachments: []types.InternetGatewayAttachment{ { State: types.AttachmentStatusAttached, - VpcId: PtrString("vpc-0d7892e00e573e701"), + VpcId: new("vpc-0d7892e00e573e701"), }, }, - InternetGatewayId: PtrString("igw-03809416c9e2fcb66"), - OwnerId: PtrString("052392120703"), + InternetGatewayId: new("igw-03809416c9e2fcb66"), + OwnerId: new("052392120703"), Tags: []types.Tag{ { - Key: PtrString("Name"), - Value: PtrString("test"), + Key: new("Name"), + Value: new("test"), }, }, }, diff --git a/aws-source/adapters/ec2-key-pair_test.go b/aws-source/adapters/ec2-key-pair_test.go index 780daf80..2cef251f 100644 --- a/aws-source/adapters/ec2-key-pair_test.go +++ b/aws-source/adapters/ec2-key-pair_test.go @@ -42,13 +42,13 @@ func TestKeyPairOutputMapper(t *testing.T) { output := &ec2.DescribeKeyPairsOutput{ KeyPairs: []types.KeyPairInfo{ { - KeyPairId: PtrString("key-04d7068d3a33bf9b2"), - KeyFingerprint: PtrString("df:73:bb:86:a7:cd:9e:18:16:10:50:79:fa:3b:4f:c7:1d:32:cf:58"), - KeyName: PtrString("dylan.ratcliffe"), + KeyPairId: new("key-04d7068d3a33bf9b2"), + KeyFingerprint: new("df:73:bb:86:a7:cd:9e:18:16:10:50:79:fa:3b:4f:c7:1d:32:cf:58"), + KeyName: new("dylan.ratcliffe"), KeyType: types.KeyTypeRsa, Tags: []types.Tag{}, - CreateTime: PtrTime(time.Now()), - PublicKey: PtrString("PUB"), + CreateTime: new(time.Now()), + PublicKey: new("PUB"), }, }, } diff --git a/aws-source/adapters/ec2-launch-template-version_test.go b/aws-source/adapters/ec2-launch-template-version_test.go index 32ac3182..3efb7b63 100644 --- a/aws-source/adapters/ec2-launch-template-version_test.go +++ b/aws-source/adapters/ec2-launch-template-version_test.go @@ -47,48 +47,48 @@ func TestLaunchTemplateVersionOutputMapper(t *testing.T) { output := &ec2.DescribeLaunchTemplateVersionsOutput{ LaunchTemplateVersions: []types.LaunchTemplateVersion{ { - LaunchTemplateId: PtrString("lt-015547202038ae102"), - LaunchTemplateName: PtrString("test"), - VersionNumber: PtrInt64(1), - CreateTime: PtrTime(time.Now()), - CreatedBy: PtrString("arn:aws:sts::052392120703:assumed-role/AWSReservedSSO_AWSAdministratorAccess_c1c3c9c54821c68a/dylan@overmind.tech"), - DefaultVersion: PtrBool(true), + LaunchTemplateId: new("lt-015547202038ae102"), + LaunchTemplateName: new("test"), + VersionNumber: new(int64(1)), + CreateTime: new(time.Now()), + CreatedBy: new("arn:aws:sts::052392120703:assumed-role/AWSReservedSSO_AWSAdministratorAccess_c1c3c9c54821c68a/dylan@overmind.tech"), + DefaultVersion: new(true), LaunchTemplateData: &types.ResponseLaunchTemplateData{ NetworkInterfaces: []types.LaunchTemplateInstanceNetworkInterfaceSpecification{ { Ipv6Addresses: []types.InstanceIpv6Address{ { - Ipv6Address: PtrString("ipv6"), + Ipv6Address: new("ipv6"), }, }, - NetworkInterfaceId: PtrString("networkInterface"), + NetworkInterfaceId: new("networkInterface"), PrivateIpAddresses: []types.PrivateIpAddressSpecification{ { - Primary: PtrBool(true), - PrivateIpAddress: PtrString("ip"), + Primary: new(true), + PrivateIpAddress: new("ip"), }, }, - SubnetId: PtrString("subnet"), - DeviceIndex: PtrInt32(0), + SubnetId: new("subnet"), + DeviceIndex: new(int32(0)), Groups: []string{ "sg-094e151c9fc5da181", }, }, }, - ImageId: PtrString("ami-084e8c05825742534"), + ImageId: new("ami-084e8c05825742534"), InstanceType: types.InstanceTypeT1Micro, - KeyName: PtrString("dylan.ratcliffe"), + KeyName: new("dylan.ratcliffe"), BlockDeviceMappings: []types.LaunchTemplateBlockDeviceMapping{ { Ebs: &types.LaunchTemplateEbsBlockDevice{ - SnapshotId: PtrString("snap"), + SnapshotId: new("snap"), }, }, }, CapacityReservationSpecification: &types.LaunchTemplateCapacityReservationSpecificationResponse{ CapacityReservationPreference: types.CapacityReservationPreferenceNone, CapacityReservationTarget: &types.CapacityReservationTargetResponse{ - CapacityReservationId: PtrString("cap"), + CapacityReservationId: new("cap"), }, }, CpuOptions: &types.LaunchTemplateCpuOptions{}, @@ -97,9 +97,9 @@ func TestLaunchTemplateVersionOutputMapper(t *testing.T) { EnclaveOptions: &types.LaunchTemplateEnclaveOptions{}, ElasticInferenceAccelerators: []types.LaunchTemplateElasticInferenceAcceleratorResponse{}, Placement: &types.LaunchTemplatePlacement{ - AvailabilityZone: PtrString("foo"), - GroupId: PtrString("placement"), - HostId: PtrString("host"), + AvailabilityZone: new("foo"), + GroupId: new("placement"), + HostId: new("host"), }, SecurityGroupIds: []string{ "secGroup", diff --git a/aws-source/adapters/ec2-launch-template_test.go b/aws-source/adapters/ec2-launch-template_test.go index afa43420..af5f58bb 100644 --- a/aws-source/adapters/ec2-launch-template_test.go +++ b/aws-source/adapters/ec2-launch-template_test.go @@ -42,12 +42,12 @@ func TestLaunchTemplateOutputMapper(t *testing.T) { output := &ec2.DescribeLaunchTemplatesOutput{ LaunchTemplates: []types.LaunchTemplate{ { - CreateTime: PtrTime(time.Now()), - CreatedBy: PtrString("me"), - DefaultVersionNumber: PtrInt64(1), - LatestVersionNumber: PtrInt64(10), - LaunchTemplateId: PtrString("id"), - LaunchTemplateName: PtrString("hello"), + CreateTime: new(time.Now()), + CreatedBy: new("me"), + DefaultVersionNumber: new(int64(1)), + LatestVersionNumber: new(int64(10)), + LaunchTemplateId: new("id"), + LaunchTemplateName: new("hello"), Tags: []types.Tag{}, }, }, diff --git a/aws-source/adapters/ec2-nat-gateway_test.go b/aws-source/adapters/ec2-nat-gateway_test.go index 88e739e5..2e5cf42d 100644 --- a/aws-source/adapters/ec2-nat-gateway_test.go +++ b/aws-source/adapters/ec2-nat-gateway_test.go @@ -43,47 +43,47 @@ func TestNatGatewayOutputMapper(t *testing.T) { output := &ec2.DescribeNatGatewaysOutput{ NatGateways: []types.NatGateway{ { - CreateTime: PtrTime(time.Now()), - DeleteTime: PtrTime(time.Now()), - FailureCode: PtrString("Gateway.NotAttached"), - FailureMessage: PtrString("Network vpc-0d7892e00e573e701 has no Internet gateway attached"), + CreateTime: new(time.Now()), + DeleteTime: new(time.Now()), + FailureCode: new("Gateway.NotAttached"), + FailureMessage: new("Network vpc-0d7892e00e573e701 has no Internet gateway attached"), NatGatewayAddresses: []types.NatGatewayAddress{ { - AllocationId: PtrString("eipalloc-000a9739291350592"), - NetworkInterfaceId: PtrString("eni-0c59532b8e10343ae"), - PrivateIp: PtrString("172.31.89.23"), + AllocationId: new("eipalloc-000a9739291350592"), + NetworkInterfaceId: new("eni-0c59532b8e10343ae"), + PrivateIp: new("172.31.89.23"), }, }, - NatGatewayId: PtrString("nat-0e4e73d7ac46af25e"), + NatGatewayId: new("nat-0e4e73d7ac46af25e"), State: types.NatGatewayStateFailed, - SubnetId: PtrString("subnet-0450a637af9984235"), - VpcId: PtrString("vpc-0d7892e00e573e701"), + SubnetId: new("subnet-0450a637af9984235"), + VpcId: new("vpc-0d7892e00e573e701"), Tags: []types.Tag{ { - Key: PtrString("Name"), - Value: PtrString("test"), + Key: new("Name"), + Value: new("test"), }, }, ConnectivityType: types.ConnectivityTypePublic, }, { - CreateTime: PtrTime(time.Now()), + CreateTime: new(time.Now()), NatGatewayAddresses: []types.NatGatewayAddress{ { - AllocationId: PtrString("eipalloc-000a9739291350592"), - NetworkInterfaceId: PtrString("eni-0b4652e6f2aa36d78"), - PrivateIp: PtrString("172.31.35.98"), - PublicIp: PtrString("18.170.133.9"), + AllocationId: new("eipalloc-000a9739291350592"), + NetworkInterfaceId: new("eni-0b4652e6f2aa36d78"), + PrivateIp: new("172.31.35.98"), + PublicIp: new("18.170.133.9"), }, }, - NatGatewayId: PtrString("nat-0e07f7530ef076766"), + NatGatewayId: new("nat-0e07f7530ef076766"), State: types.NatGatewayStateAvailable, - SubnetId: PtrString("subnet-0d8ae4b4e07647efa"), - VpcId: PtrString("vpc-0d7892e00e573e701"), + SubnetId: new("subnet-0d8ae4b4e07647efa"), + VpcId: new("vpc-0d7892e00e573e701"), Tags: []types.Tag{ { - Key: PtrString("Name"), - Value: PtrString("test"), + Key: new("Name"), + Value: new("test"), }, }, ConnectivityType: types.ConnectivityTypePublic, diff --git a/aws-source/adapters/ec2-network-acl_test.go b/aws-source/adapters/ec2-network-acl_test.go index fba190e0..4b34b1dc 100644 --- a/aws-source/adapters/ec2-network-acl_test.go +++ b/aws-source/adapters/ec2-network-acl_test.go @@ -45,42 +45,42 @@ func TestNetworkAclOutputMapper(t *testing.T) { { Associations: []types.NetworkAclAssociation{ { - NetworkAclAssociationId: PtrString("aclassoc-0f85f8b1fde0a5939"), - NetworkAclId: PtrString("acl-0a346e8e6f5a9ad91"), - SubnetId: PtrString("subnet-0450a637af9984235"), + NetworkAclAssociationId: new("aclassoc-0f85f8b1fde0a5939"), + NetworkAclId: new("acl-0a346e8e6f5a9ad91"), + SubnetId: new("subnet-0450a637af9984235"), }, { - NetworkAclAssociationId: PtrString("aclassoc-064b78003a2d309a4"), - NetworkAclId: PtrString("acl-0a346e8e6f5a9ad91"), - SubnetId: PtrString("subnet-06c0dea0437180c61"), + NetworkAclAssociationId: new("aclassoc-064b78003a2d309a4"), + NetworkAclId: new("acl-0a346e8e6f5a9ad91"), + SubnetId: new("subnet-06c0dea0437180c61"), }, { - NetworkAclAssociationId: PtrString("aclassoc-0575080579a7381f5"), - NetworkAclId: PtrString("acl-0a346e8e6f5a9ad91"), - SubnetId: PtrString("subnet-0d8ae4b4e07647efa"), + NetworkAclAssociationId: new("aclassoc-0575080579a7381f5"), + NetworkAclId: new("acl-0a346e8e6f5a9ad91"), + SubnetId: new("subnet-0d8ae4b4e07647efa"), }, }, Entries: []types.NetworkAclEntry{ { - CidrBlock: PtrString("0.0.0.0/0"), - Egress: PtrBool(true), - Protocol: PtrString("-1"), + CidrBlock: new("0.0.0.0/0"), + Egress: new(true), + Protocol: new("-1"), RuleAction: types.RuleActionAllow, - RuleNumber: PtrInt32(100), + RuleNumber: new(int32(100)), }, { - CidrBlock: PtrString("0.0.0.0/0"), - Egress: PtrBool(true), - Protocol: PtrString("-1"), + CidrBlock: new("0.0.0.0/0"), + Egress: new(true), + Protocol: new("-1"), RuleAction: types.RuleActionDeny, - RuleNumber: PtrInt32(32767), + RuleNumber: new(int32(32767)), }, }, - IsDefault: PtrBool(true), - NetworkAclId: PtrString("acl-0a346e8e6f5a9ad91"), + IsDefault: new(true), + NetworkAclId: new("acl-0a346e8e6f5a9ad91"), Tags: []types.Tag{}, - VpcId: PtrString("vpc-0d7892e00e573e701"), - OwnerId: PtrString("052392120703"), + VpcId: new("vpc-0d7892e00e573e701"), + OwnerId: new("052392120703"), }, }, } diff --git a/aws-source/adapters/ec2-network-interface-permission_test.go b/aws-source/adapters/ec2-network-interface-permission_test.go index 93104d99..7906de40 100644 --- a/aws-source/adapters/ec2-network-interface-permission_test.go +++ b/aws-source/adapters/ec2-network-interface-permission_test.go @@ -43,9 +43,9 @@ func TestNetworkInterfacePermissionOutputMapper(t *testing.T) { output := &ec2.DescribeNetworkInterfacePermissionsOutput{ NetworkInterfacePermissions: []types.NetworkInterfacePermission{ { - NetworkInterfacePermissionId: PtrString("eni-perm-0b6211455242c105e"), - NetworkInterfaceId: PtrString("eni-07f8f3d404036c833"), - AwsService: PtrString("routing.hyperplane.eu-west-2.amazonaws.com"), + NetworkInterfacePermissionId: new("eni-perm-0b6211455242c105e"), + NetworkInterfaceId: new("eni-07f8f3d404036c833"), + AwsService: new("routing.hyperplane.eu-west-2.amazonaws.com"), Permission: types.InterfacePermissionTypeInstanceAttach, PermissionState: &types.NetworkInterfacePermissionState{ State: types.NetworkInterfacePermissionStateCodeGranted, diff --git a/aws-source/adapters/ec2-network-interface_test.go b/aws-source/adapters/ec2-network-interface_test.go index 2a03331f..d3066cac 100644 --- a/aws-source/adapters/ec2-network-interface_test.go +++ b/aws-source/adapters/ec2-network-interface_test.go @@ -126,62 +126,62 @@ func TestNetworkInterfaceOutputMapper(t *testing.T) { NetworkInterfaces: []types.NetworkInterface{ { Association: &types.NetworkInterfaceAssociation{ - AllocationId: PtrString("eipalloc-000a9739291350592"), - AssociationId: PtrString("eipassoc-049cda1f947e5efe6"), - IpOwnerId: PtrString("052392120703"), - PublicDnsName: PtrString("ec2-18-170-133-9.eu-west-2.compute.amazonaws.com"), - PublicIp: PtrString("18.170.133.9"), + AllocationId: new("eipalloc-000a9739291350592"), + AssociationId: new("eipassoc-049cda1f947e5efe6"), + IpOwnerId: new("052392120703"), + PublicDnsName: new("ec2-18-170-133-9.eu-west-2.compute.amazonaws.com"), + PublicIp: new("18.170.133.9"), }, Attachment: &types.NetworkInterfaceAttachment{ - AttachmentId: PtrString("ela-attach-03e560efca8c9e5d8"), - DeleteOnTermination: PtrBool(false), - DeviceIndex: PtrInt32(1), - InstanceOwnerId: PtrString("amazon-aws"), + AttachmentId: new("ela-attach-03e560efca8c9e5d8"), + DeleteOnTermination: new(false), + DeviceIndex: new(int32(1)), + InstanceOwnerId: new("amazon-aws"), Status: types.AttachmentStatusAttached, - InstanceId: PtrString("foo"), + InstanceId: new("foo"), }, - AvailabilityZone: PtrString("eu-west-2b"), - Description: PtrString("Interface for NAT Gateway nat-0e07f7530ef076766"), + AvailabilityZone: new("eu-west-2b"), + Description: new("Interface for NAT Gateway nat-0e07f7530ef076766"), Groups: []types.GroupIdentifier{ { - GroupId: PtrString("group-123"), - GroupName: PtrString("something"), + GroupId: new("group-123"), + GroupName: new("something"), }, }, InterfaceType: types.NetworkInterfaceTypeNatGateway, Ipv6Addresses: []types.NetworkInterfaceIpv6Address{ { - Ipv6Address: PtrString("2001:db8:1234:0000:0000:0000:0000:0000"), + Ipv6Address: new("2001:db8:1234:0000:0000:0000:0000:0000"), }, }, - MacAddress: PtrString("0a:f4:55:b0:6c:be"), - NetworkInterfaceId: PtrString("eni-0b4652e6f2aa36d78"), - OwnerId: PtrString("052392120703"), - PrivateDnsName: PtrString("ip-172-31-35-98.eu-west-2.compute.internal"), - PrivateIpAddress: PtrString("172.31.35.98"), + MacAddress: new("0a:f4:55:b0:6c:be"), + NetworkInterfaceId: new("eni-0b4652e6f2aa36d78"), + OwnerId: new("052392120703"), + PrivateDnsName: new("ip-172-31-35-98.eu-west-2.compute.internal"), + PrivateIpAddress: new("172.31.35.98"), PrivateIpAddresses: []types.NetworkInterfacePrivateIpAddress{ { Association: &types.NetworkInterfaceAssociation{ - AllocationId: PtrString("eipalloc-000a9739291350592"), - AssociationId: PtrString("eipassoc-049cda1f947e5efe6"), - IpOwnerId: PtrString("052392120703"), - PublicDnsName: PtrString("ec2-18-170-133-9.eu-west-2.compute.amazonaws.com"), - PublicIp: PtrString("18.170.133.9"), - CarrierIp: PtrString("18.170.133.10"), - CustomerOwnedIp: PtrString("18.170.133.11"), + AllocationId: new("eipalloc-000a9739291350592"), + AssociationId: new("eipassoc-049cda1f947e5efe6"), + IpOwnerId: new("052392120703"), + PublicDnsName: new("ec2-18-170-133-9.eu-west-2.compute.amazonaws.com"), + PublicIp: new("18.170.133.9"), + CarrierIp: new("18.170.133.10"), + CustomerOwnedIp: new("18.170.133.11"), }, - Primary: PtrBool(true), - PrivateDnsName: PtrString("ip-172-31-35-98.eu-west-2.compute.internal"), - PrivateIpAddress: PtrString("172.31.35.98"), + Primary: new(true), + PrivateDnsName: new("ip-172-31-35-98.eu-west-2.compute.internal"), + PrivateIpAddress: new("172.31.35.98"), }, }, - RequesterId: PtrString("440527171281"), - RequesterManaged: PtrBool(true), - SourceDestCheck: PtrBool(false), + RequesterId: new("440527171281"), + RequesterManaged: new(true), + SourceDestCheck: new(false), Status: types.NetworkInterfaceStatusInUse, - SubnetId: PtrString("subnet-0d8ae4b4e07647efa"), + SubnetId: new("subnet-0d8ae4b4e07647efa"), TagSet: []types.Tag{}, - VpcId: PtrString("vpc-0d7892e00e573e701"), + VpcId: new("vpc-0d7892e00e573e701"), }, }, } diff --git a/aws-source/adapters/ec2-placement-group_test.go b/aws-source/adapters/ec2-placement-group_test.go index ca79deb5..ea72ca7f 100644 --- a/aws-source/adapters/ec2-placement-group_test.go +++ b/aws-source/adapters/ec2-placement-group_test.go @@ -42,13 +42,13 @@ func TestPlacementGroupOutputMapper(t *testing.T) { output := &ec2.DescribePlacementGroupsOutput{ PlacementGroups: []types.PlacementGroup{ { - GroupArn: PtrString("arn"), - GroupId: PtrString("id"), - GroupName: PtrString("name"), + GroupArn: new("arn"), + GroupId: new("id"), + GroupName: new("name"), SpreadLevel: types.SpreadLevelHost, State: types.PlacementGroupStateAvailable, Strategy: types.PlacementStrategyCluster, - PartitionCount: PtrInt32(1), + PartitionCount: new(int32(1)), Tags: []types.Tag{}, }, }, diff --git a/aws-source/adapters/ec2-reserved-instance_test.go b/aws-source/adapters/ec2-reserved-instance_test.go index 9a381de8..31450d4e 100644 --- a/aws-source/adapters/ec2-reserved-instance_test.go +++ b/aws-source/adapters/ec2-reserved-instance_test.go @@ -42,12 +42,12 @@ func TestReservedInstanceOutputMapper(t *testing.T) { output := &ec2.DescribeReservedInstancesOutput{ ReservedInstances: []types.ReservedInstances{ { - AvailabilityZone: PtrString("az"), + AvailabilityZone: new("az"), CurrencyCode: types.CurrencyCodeValuesUsd, - Duration: PtrInt64(100), - End: PtrTime(time.Now()), - FixedPrice: PtrFloat32(1.23), - InstanceCount: PtrInt32(1), + Duration: new(int64(100)), + End: new(time.Now()), + FixedPrice: new(float32(1.23)), + InstanceCount: new(int32(1)), InstanceTenancy: types.TenancyDedicated, InstanceType: types.InstanceTypeA14xlarge, OfferingClass: types.OfferingClassTypeConvertible, @@ -55,15 +55,15 @@ func TestReservedInstanceOutputMapper(t *testing.T) { ProductDescription: types.RIProductDescription("foo"), RecurringCharges: []types.RecurringCharge{ { - Amount: PtrFloat64(1.111), + Amount: new(1.111), Frequency: types.RecurringChargeFrequencyHourly, }, }, - ReservedInstancesId: PtrString("id"), + ReservedInstancesId: new("id"), Scope: types.ScopeAvailabilityZone, - Start: PtrTime(time.Now()), + Start: new(time.Now()), State: types.ReservedInstanceStateActive, - UsagePrice: PtrFloat32(99.00000001), + UsagePrice: new(float32(99.00000001)), }, }, } diff --git a/aws-source/adapters/ec2-route-table_test.go b/aws-source/adapters/ec2-route-table_test.go index 7d5d1875..96878600 100644 --- a/aws-source/adapters/ec2-route-table_test.go +++ b/aws-source/adapters/ec2-route-table_test.go @@ -45,11 +45,11 @@ func TestRouteTableOutputMapper(t *testing.T) { { Associations: []types.RouteTableAssociation{ { - Main: PtrBool(false), - RouteTableAssociationId: PtrString("rtbassoc-0aa1442039abff3db"), - RouteTableId: PtrString("rtb-00b1197fa95a6b35f"), - SubnetId: PtrString("subnet-06c0dea0437180c61"), - GatewayId: PtrString("ID"), + Main: new(false), + RouteTableAssociationId: new("rtbassoc-0aa1442039abff3db"), + RouteTableId: new("rtb-00b1197fa95a6b35f"), + SubnetId: new("subnet-06c0dea0437180c61"), + GatewayId: new("ID"), AssociationState: &types.RouteTableAssociationState{ State: types.RouteTableAssociationStateCodeAssociated, }, @@ -57,35 +57,35 @@ func TestRouteTableOutputMapper(t *testing.T) { }, PropagatingVgws: []types.PropagatingVgw{ { - GatewayId: PtrString("goo"), + GatewayId: new("goo"), }, }, - RouteTableId: PtrString("rtb-00b1197fa95a6b35f"), + RouteTableId: new("rtb-00b1197fa95a6b35f"), Routes: []types.Route{ { - DestinationCidrBlock: PtrString("172.31.0.0/16"), - GatewayId: PtrString("igw-12345"), + DestinationCidrBlock: new("172.31.0.0/16"), + GatewayId: new("igw-12345"), Origin: types.RouteOriginCreateRouteTable, State: types.RouteStateActive, }, { - DestinationPrefixListId: PtrString("pl-7ca54015"), - GatewayId: PtrString("vpce-09fcbac4dcf142db3"), + DestinationPrefixListId: new("pl-7ca54015"), + GatewayId: new("vpce-09fcbac4dcf142db3"), Origin: types.RouteOriginCreateRoute, State: types.RouteStateActive, - CarrierGatewayId: PtrString("id"), - EgressOnlyInternetGatewayId: PtrString("id"), - InstanceId: PtrString("id"), - InstanceOwnerId: PtrString("id"), - LocalGatewayId: PtrString("id"), - NatGatewayId: PtrString("id"), - NetworkInterfaceId: PtrString("id"), - TransitGatewayId: PtrString("id"), - VpcPeeringConnectionId: PtrString("id"), + CarrierGatewayId: new("id"), + EgressOnlyInternetGatewayId: new("id"), + InstanceId: new("id"), + InstanceOwnerId: new("id"), + LocalGatewayId: new("id"), + NatGatewayId: new("id"), + NetworkInterfaceId: new("id"), + TransitGatewayId: new("id"), + VpcPeeringConnectionId: new("id"), }, }, - VpcId: PtrString("vpc-0d7892e00e573e701"), - OwnerId: PtrString("052392120703"), + VpcId: new("vpc-0d7892e00e573e701"), + OwnerId: new("052392120703"), }, }, } diff --git a/aws-source/adapters/ec2-security-group-rule_test.go b/aws-source/adapters/ec2-security-group-rule_test.go index 95465917..5523c009 100644 --- a/aws-source/adapters/ec2-security-group-rule_test.go +++ b/aws-source/adapters/ec2-security-group-rule_test.go @@ -43,33 +43,33 @@ func TestSecurityGroupRuleOutputMapper(t *testing.T) { output := &ec2.DescribeSecurityGroupRulesOutput{ SecurityGroupRules: []types.SecurityGroupRule{ { - SecurityGroupRuleId: PtrString("sgr-0b0e42d1431e832bd"), - GroupId: PtrString("sg-0814766e46f201c22"), - GroupOwnerId: PtrString("052392120703"), - IsEgress: PtrBool(false), - IpProtocol: PtrString("tcp"), - FromPort: PtrInt32(2049), - ToPort: PtrInt32(2049), + SecurityGroupRuleId: new("sgr-0b0e42d1431e832bd"), + GroupId: new("sg-0814766e46f201c22"), + GroupOwnerId: new("052392120703"), + IsEgress: new(false), + IpProtocol: new("tcp"), + FromPort: new(int32(2049)), + ToPort: new(int32(2049)), ReferencedGroupInfo: &types.ReferencedSecurityGroup{ - GroupId: PtrString("sg-09371b4a54fe7ab38"), - UserId: PtrString("052392120703"), + GroupId: new("sg-09371b4a54fe7ab38"), + UserId: new("052392120703"), }, - Description: PtrString("Created by the LIW for EFS at 2022-12-16T19:14:27.033Z"), + Description: new("Created by the LIW for EFS at 2022-12-16T19:14:27.033Z"), Tags: []types.Tag{}, }, { - SecurityGroupRuleId: PtrString("sgr-04b583a90b4fa4ada"), - GroupId: PtrString("sg-09371b4a54fe7ab38"), - GroupOwnerId: PtrString("052392120703"), - IsEgress: PtrBool(true), - IpProtocol: PtrString("tcp"), - FromPort: PtrInt32(2049), - ToPort: PtrInt32(2049), + SecurityGroupRuleId: new("sgr-04b583a90b4fa4ada"), + GroupId: new("sg-09371b4a54fe7ab38"), + GroupOwnerId: new("052392120703"), + IsEgress: new(true), + IpProtocol: new("tcp"), + FromPort: new(int32(2049)), + ToPort: new(int32(2049)), ReferencedGroupInfo: &types.ReferencedSecurityGroup{ - GroupId: PtrString("sg-0814766e46f201c22"), - UserId: PtrString("052392120703"), + GroupId: new("sg-0814766e46f201c22"), + UserId: new("052392120703"), }, - Description: PtrString("Created by the LIW for EFS at 2022-12-16T19:14:27.349Z"), + Description: new("Created by the LIW for EFS at 2022-12-16T19:14:27.349Z"), Tags: []types.Tag{}, }, }, diff --git a/aws-source/adapters/ec2-security-group_test.go b/aws-source/adapters/ec2-security-group_test.go index ca1379a4..07f8f003 100644 --- a/aws-source/adapters/ec2-security-group_test.go +++ b/aws-source/adapters/ec2-security-group_test.go @@ -43,30 +43,30 @@ func TestSecurityGroupOutputMapper(t *testing.T) { output := &ec2.DescribeSecurityGroupsOutput{ SecurityGroups: []types.SecurityGroup{ { - Description: PtrString("default VPC security group"), - GroupName: PtrString("default"), + Description: new("default VPC security group"), + GroupName: new("default"), IpPermissions: []types.IpPermission{ { - IpProtocol: PtrString("-1"), + IpProtocol: new("-1"), IpRanges: []types.IpRange{}, Ipv6Ranges: []types.Ipv6Range{}, PrefixListIds: []types.PrefixListId{}, UserIdGroupPairs: []types.UserIdGroupPair{ { - GroupId: PtrString("sg-094e151c9fc5da181"), - UserId: PtrString("052392120704"), + GroupId: new("sg-094e151c9fc5da181"), + UserId: new("052392120704"), }, }, }, }, - OwnerId: PtrString("052392120703"), - GroupId: PtrString("sg-094e151c9fc5da181"), + OwnerId: new("052392120703"), + GroupId: new("sg-094e151c9fc5da181"), IpPermissionsEgress: []types.IpPermission{ { - IpProtocol: PtrString("-1"), + IpProtocol: new("-1"), IpRanges: []types.IpRange{ { - CidrIp: PtrString("0.0.0.0/0"), + CidrIp: new("0.0.0.0/0"), }, }, Ipv6Ranges: []types.Ipv6Range{}, @@ -74,7 +74,7 @@ func TestSecurityGroupOutputMapper(t *testing.T) { UserIdGroupPairs: []types.UserIdGroupPair{}, }, }, - VpcId: PtrString("vpc-0d7892e00e573e701"), + VpcId: new("vpc-0d7892e00e573e701"), }, }, } diff --git a/aws-source/adapters/ec2-snapshot_test.go b/aws-source/adapters/ec2-snapshot_test.go index bf485224..44af9400 100644 --- a/aws-source/adapters/ec2-snapshot_test.go +++ b/aws-source/adapters/ec2-snapshot_test.go @@ -43,23 +43,23 @@ func TestSnapshotOutputMapper(t *testing.T) { output := &ec2.DescribeSnapshotsOutput{ Snapshots: []types.Snapshot{ { - DataEncryptionKeyId: PtrString("ek"), - KmsKeyId: PtrString("key"), - SnapshotId: PtrString("id"), - Description: PtrString("foo"), - Encrypted: PtrBool(false), - OutpostArn: PtrString("something"), - OwnerAlias: PtrString("something"), - OwnerId: PtrString("owner"), - Progress: PtrString("50%"), - RestoreExpiryTime: PtrTime(time.Now()), - StartTime: PtrTime(time.Now()), + DataEncryptionKeyId: new("ek"), + KmsKeyId: new("key"), + SnapshotId: new("id"), + Description: new("foo"), + Encrypted: new(false), + OutpostArn: new("something"), + OwnerAlias: new("something"), + OwnerId: new("owner"), + Progress: new("50%"), + RestoreExpiryTime: new(time.Now()), + StartTime: new(time.Now()), State: types.SnapshotStatePending, - StateMessage: PtrString("pending"), + StateMessage: new("pending"), StorageTier: types.StorageTierArchive, Tags: []types.Tag{}, - VolumeId: PtrString("volumeId"), - VolumeSize: PtrInt32(1024), + VolumeId: new("volumeId"), + VolumeSize: new(int32(1024)), }, }, } diff --git a/aws-source/adapters/ec2-subnet_test.go b/aws-source/adapters/ec2-subnet_test.go index 693afacd..cf2a9341 100644 --- a/aws-source/adapters/ec2-subnet_test.go +++ b/aws-source/adapters/ec2-subnet_test.go @@ -43,36 +43,36 @@ func TestSubnetOutputMapper(t *testing.T) { output := &ec2.DescribeSubnetsOutput{ Subnets: []types.Subnet{ { - AvailabilityZone: PtrString("eu-west-2c"), - AvailabilityZoneId: PtrString("euw2-az1"), - AvailableIpAddressCount: PtrInt32(4091), - CidrBlock: PtrString("172.31.80.0/20"), - DefaultForAz: PtrBool(false), - MapPublicIpOnLaunch: PtrBool(false), - MapCustomerOwnedIpOnLaunch: PtrBool(false), + AvailabilityZone: new("eu-west-2c"), + AvailabilityZoneId: new("euw2-az1"), + AvailableIpAddressCount: new(int32(4091)), + CidrBlock: new("172.31.80.0/20"), + DefaultForAz: new(false), + MapPublicIpOnLaunch: new(false), + MapCustomerOwnedIpOnLaunch: new(false), State: types.SubnetStateAvailable, - SubnetId: PtrString("subnet-0450a637af9984235"), - VpcId: PtrString("vpc-0d7892e00e573e701"), - OwnerId: PtrString("052392120703"), - AssignIpv6AddressOnCreation: PtrBool(false), + SubnetId: new("subnet-0450a637af9984235"), + VpcId: new("vpc-0d7892e00e573e701"), + OwnerId: new("052392120703"), + AssignIpv6AddressOnCreation: new(false), Ipv6CidrBlockAssociationSet: []types.SubnetIpv6CidrBlockAssociation{ { - AssociationId: PtrString("id-1234"), - Ipv6CidrBlock: PtrString("something"), + AssociationId: new("id-1234"), + Ipv6CidrBlock: new("something"), Ipv6CidrBlockState: &types.SubnetCidrBlockState{ State: types.SubnetCidrBlockStateCodeAssociated, - StatusMessage: PtrString("something here"), + StatusMessage: new("something here"), }, }, }, Tags: []types.Tag{}, - SubnetArn: PtrString("arn:aws:ec2:eu-west-2:052392120703:subnet/subnet-0450a637af9984235"), - EnableDns64: PtrBool(false), - Ipv6Native: PtrBool(false), + SubnetArn: new("arn:aws:ec2:eu-west-2:052392120703:subnet/subnet-0450a637af9984235"), + EnableDns64: new(false), + Ipv6Native: new(false), PrivateDnsNameOptionsOnLaunch: &types.PrivateDnsNameOptionsOnLaunch{ HostnameType: types.HostnameTypeIpName, - EnableResourceNameDnsARecord: PtrBool(false), - EnableResourceNameDnsAAAARecord: PtrBool(false), + EnableResourceNameDnsARecord: new(false), + EnableResourceNameDnsAAAARecord: new(false), }, }, }, 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 index b6447a84..fea112ec 100644 --- a/aws-source/adapters/ec2-transit-gateway-route-table-association_test.go +++ b/aws-source/adapters/ec2-transit-gateway-route-table-association_test.go @@ -33,8 +33,8 @@ func TestTransitGatewayRouteTableAssociationItemMapper(t *testing.T) { item := &transitGatewayRouteTableAssociationItem{ RouteTableID: "tgw-rtb-123", Association: types.TransitGatewayRouteTableAssociation{ - TransitGatewayAttachmentId: PtrString("tgw-attach-456"), - ResourceId: PtrString("vpc-abc"), + TransitGatewayAttachmentId: new("tgw-attach-456"), + ResourceId: new("vpc-abc"), ResourceType: types.TransitGatewayAttachmentResourceTypeVpc, State: types.TransitGatewayAssociationStateAssociated, }, 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 index 1d2e6b9f..b4526c91 100644 --- a/aws-source/adapters/ec2-transit-gateway-route-table-propagation_test.go +++ b/aws-source/adapters/ec2-transit-gateway-route-table-propagation_test.go @@ -33,8 +33,8 @@ func TestTransitGatewayRouteTablePropagationItemMapper(t *testing.T) { item := &transitGatewayRouteTablePropagationItem{ RouteTableID: "tgw-rtb-123", Propagation: types.TransitGatewayRouteTablePropagation{ - TransitGatewayAttachmentId: PtrString("tgw-attach-456"), - ResourceId: PtrString("vpc-abc"), + TransitGatewayAttachmentId: new("tgw-attach-456"), + ResourceId: new("vpc-abc"), ResourceType: types.TransitGatewayAttachmentResourceTypeVpc, State: types.TransitGatewayPropagationStateEnabled, }, diff --git a/aws-source/adapters/ec2-transit-gateway-route-table_test.go b/aws-source/adapters/ec2-transit-gateway-route-table_test.go index 2bf596fe..4234d548 100644 --- a/aws-source/adapters/ec2-transit-gateway-route-table_test.go +++ b/aws-source/adapters/ec2-transit-gateway-route-table_test.go @@ -43,13 +43,13 @@ func TestTransitGatewayRouteTableOutputMapper(t *testing.T) { output := &ec2.DescribeTransitGatewayRouteTablesOutput{ TransitGatewayRouteTables: []types.TransitGatewayRouteTable{ { - TransitGatewayRouteTableId: PtrString("tgw-rtb-0123456789abcdef0"), - TransitGatewayId: PtrString("tgw-0abc123"), + TransitGatewayRouteTableId: new("tgw-rtb-0123456789abcdef0"), + TransitGatewayId: new("tgw-0abc123"), State: types.TransitGatewayRouteTableStateAvailable, - DefaultAssociationRouteTable: PtrBool(false), - DefaultPropagationRouteTable: PtrBool(false), + DefaultAssociationRouteTable: new(false), + DefaultPropagationRouteTable: new(false), Tags: []types.Tag{ - {Key: PtrString("Name"), Value: PtrString("my-route-table")}, + {Key: new("Name"), Value: new("my-route-table")}, }, }, }, diff --git a/aws-source/adapters/ec2-transit-gateway-route.go b/aws-source/adapters/ec2-transit-gateway-route.go index c7f10314..52fa50db 100644 --- a/aws-source/adapters/ec2-transit-gateway-route.go +++ b/aws-source/adapters/ec2-transit-gateway-route.go @@ -56,7 +56,7 @@ func parseRouteQuery(query string) (routeTableID, destination string, err error) // 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"}}, + {Name: new("state"), Values: []string{"active", "blackhole"}}, } } @@ -72,7 +72,7 @@ func getTransitGatewayRoute(ctx context.Context, client *ec2.Client, _, query st out, err := client.SearchTransitGatewayRoutes(ctx, &ec2.SearchTransitGatewayRoutesInput{ TransitGatewayRouteTableId: &routeTableID, Filters: searchRoutesFilter(), - MaxResults: PtrInt32(maxSearchRoutesResults), + MaxResults: new(int32(maxSearchRoutesResults)), }) if err != nil { return nil, err @@ -112,7 +112,7 @@ func listTransitGatewayRoutes(ctx context.Context, client *ec2.Client, _ string) routeOut, err := client.SearchTransitGatewayRoutes(ctx, &ec2.SearchTransitGatewayRoutesInput{ TransitGatewayRouteTableId: &rtID, Filters: searchRoutesFilter(), - MaxResults: PtrInt32(maxSearchRoutesResults), + MaxResults: new(int32(maxSearchRoutesResults)), }) if err != nil { return nil, err @@ -135,7 +135,7 @@ func searchTransitGatewayRoutes(ctx context.Context, client *ec2.Client, _, quer routeOut, err := client.SearchTransitGatewayRoutes(ctx, &ec2.SearchTransitGatewayRoutesInput{ TransitGatewayRouteTableId: &routeTableID, Filters: searchRoutesFilter(), - MaxResults: PtrInt32(maxSearchRoutesResults), + MaxResults: new(int32(maxSearchRoutesResults)), }) if err != nil { return nil, err @@ -285,11 +285,11 @@ 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", + 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"}, diff --git a/aws-source/adapters/ec2-transit-gateway-route_test.go b/aws-source/adapters/ec2-transit-gateway-route_test.go index 888edb22..b357f7cb 100644 --- a/aws-source/adapters/ec2-transit-gateway-route_test.go +++ b/aws-source/adapters/ec2-transit-gateway-route_test.go @@ -8,10 +8,10 @@ import ( ) func TestTransitGatewayRouteDestination(t *testing.T) { - if transitGatewayRouteDestination(&types.TransitGatewayRoute{DestinationCidrBlock: PtrString("10.0.0.0/16")}) != "10.0.0.0/16" { + if transitGatewayRouteDestination(&types.TransitGatewayRoute{DestinationCidrBlock: new("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" { + if transitGatewayRouteDestination(&types.TransitGatewayRoute{PrefixListId: new("pl-123")}) != "pl:pl-123" { t.Error("expected prefix list destination") } } @@ -42,7 +42,7 @@ func TestTransitGatewayRouteItemMapper(t *testing.T) { item := &transitGatewayRouteItem{ RouteTableID: "tgw-rtb-123", Route: types.TransitGatewayRoute{ - DestinationCidrBlock: PtrString("10.0.0.0/16"), + DestinationCidrBlock: new("10.0.0.0/16"), State: types.TransitGatewayRouteStateActive, Type: types.TransitGatewayRouteTypeStatic, }, diff --git a/aws-source/adapters/ec2-volume-status_test.go b/aws-source/adapters/ec2-volume-status_test.go index a5ac6cae..15943f0a 100644 --- a/aws-source/adapters/ec2-volume-status_test.go +++ b/aws-source/adapters/ec2-volume-status_test.go @@ -45,33 +45,33 @@ func TestVolumeStatusOutputMapper(t *testing.T) { { Actions: []types.VolumeStatusAction{ { - Code: PtrString("enable-volume-io"), - Description: PtrString("Enable volume I/O"), - EventId: PtrString("12"), - EventType: PtrString("io-enabled"), + Code: new("enable-volume-io"), + Description: new("Enable volume I/O"), + EventId: new("12"), + EventType: new("io-enabled"), }, }, - AvailabilityZone: PtrString("eu-west-2c"), + AvailabilityZone: new("eu-west-2c"), Events: []types.VolumeStatusEvent{ { - Description: PtrString("The volume is operating normally"), - EventId: PtrString("12"), - EventType: PtrString("io-enabled"), - InstanceId: PtrString("i-0667d3ca802741e30"), // link - NotAfter: PtrTime(time.Now()), - NotBefore: PtrTime(time.Now()), + Description: new("The volume is operating normally"), + EventId: new("12"), + EventType: new("io-enabled"), + InstanceId: new("i-0667d3ca802741e30"), // link + NotAfter: new(time.Now()), + NotBefore: new(time.Now()), }, }, - VolumeId: PtrString("vol-0a38796ac85e21c11"), // link + VolumeId: new("vol-0a38796ac85e21c11"), // link VolumeStatus: &types.VolumeStatusInfo{ Details: []types.VolumeStatusDetails{ { Name: types.VolumeStatusNameIoEnabled, - Status: PtrString("passed"), + Status: new("passed"), }, { Name: types.VolumeStatusNameIoPerformance, - Status: PtrString("not-applicable"), + Status: new("not-applicable"), }, }, Status: types.VolumeStatusInfoStatusOk, diff --git a/aws-source/adapters/ec2-volume_test.go b/aws-source/adapters/ec2-volume_test.go index 277c8dbb..46526bb2 100644 --- a/aws-source/adapters/ec2-volume_test.go +++ b/aws-source/adapters/ec2-volume_test.go @@ -45,24 +45,24 @@ func TestVolumeOutputMapper(t *testing.T) { { Attachments: []types.VolumeAttachment{ { - AttachTime: PtrTime(time.Now()), - Device: PtrString("/dev/sdb"), - InstanceId: PtrString("i-0667d3ca802741e30"), + AttachTime: new(time.Now()), + Device: new("/dev/sdb"), + InstanceId: new("i-0667d3ca802741e30"), State: types.VolumeAttachmentStateAttaching, - VolumeId: PtrString("vol-0eae6976b359d8825"), - DeleteOnTermination: PtrBool(false), + VolumeId: new("vol-0eae6976b359d8825"), + DeleteOnTermination: new(false), }, }, - AvailabilityZone: PtrString("eu-west-2c"), - CreateTime: PtrTime(time.Now()), - Encrypted: PtrBool(false), - Size: PtrInt32(8), + AvailabilityZone: new("eu-west-2c"), + CreateTime: new(time.Now()), + Encrypted: new(false), + Size: new(int32(8)), State: types.VolumeStateInUse, - VolumeId: PtrString("vol-0eae6976b359d8825"), - Iops: PtrInt32(3000), + VolumeId: new("vol-0eae6976b359d8825"), + Iops: new(int32(3000)), VolumeType: types.VolumeTypeGp3, - MultiAttachEnabled: PtrBool(false), - Throughput: PtrInt32(125), + MultiAttachEnabled: new(false), + Throughput: new(int32(125)), }, }, } diff --git a/aws-source/adapters/ec2-vpc-endpoint_test.go b/aws-source/adapters/ec2-vpc-endpoint_test.go index 8a471d13..c12a170d 100644 --- a/aws-source/adapters/ec2-vpc-endpoint_test.go +++ b/aws-source/adapters/ec2-vpc-endpoint_test.go @@ -31,13 +31,13 @@ func TestVpcEndpointOutputMapper(t *testing.T) { output := &ec2.DescribeVpcEndpointsOutput{ VpcEndpoints: []types.VpcEndpoint{ { - VpcEndpointId: PtrString("vpce-0d7892e00e573e701"), + VpcEndpointId: new("vpce-0d7892e00e573e701"), VpcEndpointType: types.VpcEndpointTypeInterface, - CreationTimestamp: PtrTime(time.Now()), - VpcId: PtrString("vpc-0d7892e00e573e701"), // link - ServiceName: PtrString("com.amazonaws.us-east-1.s3"), + CreationTimestamp: new(time.Now()), + VpcId: new("vpc-0d7892e00e573e701"), // link + ServiceName: new("com.amazonaws.us-east-1.s3"), State: types.StateAvailable, - PolicyDocument: PtrString("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"*\",\"Resource\":\"*\",\"Effect\":\"Allow\",\"Principal\":\"*\"},{\"Condition\":{\"StringNotEquals\":{\"aws:PrincipalAccount\":\"944651592624\"}},\"Action\":\"*\",\"Resource\":\"*\",\"Effect\":\"Deny\",\"Principal\":\"*\"}]}"), // parse this + PolicyDocument: new("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Action\":\"*\",\"Resource\":\"*\",\"Effect\":\"Allow\",\"Principal\":\"*\"},{\"Condition\":{\"StringNotEquals\":{\"aws:PrincipalAccount\":\"944651592624\"}},\"Action\":\"*\",\"Resource\":\"*\",\"Effect\":\"Deny\",\"Principal\":\"*\"}]}"), // parse this RouteTableIds: []string{ "rtb-0d7892e00e573e701", // link }, @@ -46,35 +46,35 @@ func TestVpcEndpointOutputMapper(t *testing.T) { }, Groups: []types.SecurityGroupIdentifier{ { - GroupId: PtrString("sg-0d7892e00e573e701"), // link - GroupName: PtrString("default"), + GroupId: new("sg-0d7892e00e573e701"), // link + GroupName: new("default"), }, }, IpAddressType: types.IpAddressTypeIpv4, - PrivateDnsEnabled: PtrBool(true), - RequesterManaged: PtrBool(false), + PrivateDnsEnabled: new(true), + RequesterManaged: new(false), DnsEntries: []types.DnsEntry{ { - DnsName: PtrString("vpce-0d7892e00e573e701-123456789012.us-east-1.vpce.amazonaws.com"), // link - HostedZoneId: PtrString("Z2F56UZL2M1ACD"), // link + DnsName: new("vpce-0d7892e00e573e701-123456789012.us-east-1.vpce.amazonaws.com"), // link + HostedZoneId: new("Z2F56UZL2M1ACD"), // link }, }, DnsOptions: &types.DnsOptions{ DnsRecordIpType: types.DnsRecordIpTypeDualstack, - PrivateDnsOnlyForInboundResolverEndpoint: PtrBool(false), + PrivateDnsOnlyForInboundResolverEndpoint: new(false), }, LastError: &types.LastError{ - Code: PtrString("Client::ValidationException"), - Message: PtrString("The security group 'sg-0d7892e00e573e701' does not exist"), + Code: new("Client::ValidationException"), + Message: new("The security group 'sg-0d7892e00e573e701' does not exist"), }, NetworkInterfaceIds: []string{ "eni-0d7892e00e573e701", // link }, - OwnerId: PtrString("052392120703"), + OwnerId: new("052392120703"), Tags: []types.Tag{ { - Key: PtrString("Name"), - Value: PtrString("my-vpce"), + Key: new("Name"), + Value: new("my-vpce"), }, }, }, diff --git a/aws-source/adapters/ec2-vpc-peering-connection_test.go b/aws-source/adapters/ec2-vpc-peering-connection_test.go index 6b2b6da3..949f5179 100644 --- a/aws-source/adapters/ec2-vpc-peering-connection_test.go +++ b/aws-source/adapters/ec2-vpc-peering-connection_test.go @@ -15,48 +15,48 @@ func TestVpcPeeringConnectionOutputMapper(t *testing.T) { output := &ec2.DescribeVpcPeeringConnectionsOutput{ VpcPeeringConnections: []types.VpcPeeringConnection{ { - VpcPeeringConnectionId: PtrString("pcx-1234567890"), + VpcPeeringConnectionId: new("pcx-1234567890"), Status: &types.VpcPeeringConnectionStateReason{ Code: types.VpcPeeringConnectionStateReasonCodeActive, // health - Message: PtrString("message"), + Message: new("message"), }, AccepterVpcInfo: &types.VpcPeeringConnectionVpcInfo{ - CidrBlock: PtrString("10.0.0.1/24"), + CidrBlock: new("10.0.0.1/24"), CidrBlockSet: []types.CidrBlock{ { - CidrBlock: PtrString("10.0.2.1/24"), + CidrBlock: new("10.0.2.1/24"), }, }, Ipv6CidrBlockSet: []types.Ipv6CidrBlock{ { - Ipv6CidrBlock: PtrString("::/64"), + Ipv6CidrBlock: new("::/64"), }, }, - OwnerId: PtrString("123456789012"), - Region: PtrString("eu-west-2"), // link - VpcId: PtrString("vpc-1234567890"), // link + OwnerId: new("123456789012"), + Region: new("eu-west-2"), // link + VpcId: new("vpc-1234567890"), // link PeeringOptions: &types.VpcPeeringConnectionOptionsDescription{ - AllowDnsResolutionFromRemoteVpc: PtrBool(true), + AllowDnsResolutionFromRemoteVpc: new(true), }, }, RequesterVpcInfo: &types.VpcPeeringConnectionVpcInfo{ - CidrBlock: PtrString("10.0.0.1/24"), + CidrBlock: new("10.0.0.1/24"), CidrBlockSet: []types.CidrBlock{ { - CidrBlock: PtrString("10.0.2.1/24"), + CidrBlock: new("10.0.2.1/24"), }, }, Ipv6CidrBlockSet: []types.Ipv6CidrBlock{ { - Ipv6CidrBlock: PtrString("::/64"), + Ipv6CidrBlock: new("::/64"), }, }, - OwnerId: PtrString("987654321098"), + OwnerId: new("987654321098"), PeeringOptions: &types.VpcPeeringConnectionOptionsDescription{ - AllowDnsResolutionFromRemoteVpc: PtrBool(true), + AllowDnsResolutionFromRemoteVpc: new(true), }, - Region: PtrString("eu-west-5"), // link - VpcId: PtrString("vpc-9887654321"), // link + Region: new("eu-west-5"), // link + VpcId: new("vpc-9887654321"), // link }, }, }, diff --git a/aws-source/adapters/ec2-vpc_test.go b/aws-source/adapters/ec2-vpc_test.go index fea243cd..76c22809 100644 --- a/aws-source/adapters/ec2-vpc_test.go +++ b/aws-source/adapters/ec2-vpc_test.go @@ -42,38 +42,38 @@ func TestVpcOutputMapper(t *testing.T) { output := &ec2.DescribeVpcsOutput{ Vpcs: []types.Vpc{ { - CidrBlock: PtrString("172.31.0.0/16"), - DhcpOptionsId: PtrString("dopt-0959b838bf4a4c7b8"), + CidrBlock: new("172.31.0.0/16"), + DhcpOptionsId: new("dopt-0959b838bf4a4c7b8"), State: types.VpcStateAvailable, - VpcId: PtrString("vpc-0d7892e00e573e701"), - OwnerId: PtrString("052392120703"), + VpcId: new("vpc-0d7892e00e573e701"), + OwnerId: new("052392120703"), InstanceTenancy: types.TenancyDefault, CidrBlockAssociationSet: []types.VpcCidrBlockAssociation{ { - AssociationId: PtrString("vpc-cidr-assoc-0b77866f37f500af6"), - CidrBlock: PtrString("172.31.0.0/16"), + AssociationId: new("vpc-cidr-assoc-0b77866f37f500af6"), + CidrBlock: new("172.31.0.0/16"), CidrBlockState: &types.VpcCidrBlockState{ State: types.VpcCidrBlockStateCodeAssociated, }, }, }, - IsDefault: PtrBool(false), + IsDefault: new(false), Tags: []types.Tag{ { - Key: PtrString("aws:cloudformation:logical-id"), - Value: PtrString("VPC"), + Key: new("aws:cloudformation:logical-id"), + Value: new("VPC"), }, { - Key: PtrString("aws:cloudformation:stack-id"), - Value: PtrString("arn:aws:cloudformation:eu-west-2:052392120703:stack/StackSet-AWSControlTowerBP-VPC-ACCOUNT-FACTORY-V1-8c2a9348-a30c-4ac3-94c2-8279157c9243/ccde3240-7afa-11ed-81ff-02845d4c2702"), + Key: new("aws:cloudformation:stack-id"), + Value: new("arn:aws:cloudformation:eu-west-2:052392120703:stack/StackSet-AWSControlTowerBP-VPC-ACCOUNT-FACTORY-V1-8c2a9348-a30c-4ac3-94c2-8279157c9243/ccde3240-7afa-11ed-81ff-02845d4c2702"), }, { - Key: PtrString("aws:cloudformation:stack-name"), - Value: PtrString("StackSet-AWSControlTowerBP-VPC-ACCOUNT-FACTORY-V1-8c2a9348-a30c-4ac3-94c2-8279157c9243"), + Key: new("aws:cloudformation:stack-name"), + Value: new("StackSet-AWSControlTowerBP-VPC-ACCOUNT-FACTORY-V1-8c2a9348-a30c-4ac3-94c2-8279157c9243"), }, { - Key: PtrString("Name"), - Value: PtrString("aws-controltower-VPC"), + Key: new("Name"), + Value: new("aws-controltower-VPC"), }, }, }, diff --git a/aws-source/adapters/ecs-capacity-provider_test.go b/aws-source/adapters/ecs-capacity-provider_test.go index 77724230..41512cd3 100644 --- a/aws-source/adapters/ecs-capacity-provider_test.go +++ b/aws-source/adapters/ecs-capacity-provider_test.go @@ -17,42 +17,42 @@ func (t *ecsTestClient) DescribeCapacityProviders(ctx context.Context, params *e "": { CapacityProviders: []types.CapacityProvider{ { - CapacityProviderArn: PtrString("arn:aws:ecs:eu-west-2:052392120703:capacity-provider/FARGATE"), - Name: PtrString("FARGATE"), + CapacityProviderArn: new("arn:aws:ecs:eu-west-2:052392120703:capacity-provider/FARGATE"), + Name: new("FARGATE"), Status: types.CapacityProviderStatusActive, }, }, - NextToken: PtrString("one"), + NextToken: new("one"), }, "one": { CapacityProviders: []types.CapacityProvider{ { - CapacityProviderArn: PtrString("arn:aws:ecs:eu-west-2:052392120703:capacity-provider/FARGATE_SPOT"), - Name: PtrString("FARGATE_SPOT"), + CapacityProviderArn: new("arn:aws:ecs:eu-west-2:052392120703:capacity-provider/FARGATE_SPOT"), + Name: new("FARGATE_SPOT"), Status: types.CapacityProviderStatusActive, }, }, - NextToken: PtrString("two"), + NextToken: new("two"), }, "two": { CapacityProviders: []types.CapacityProvider{ { - CapacityProviderArn: PtrString("arn:aws:ecs:eu-west-2:052392120703:capacity-provider/test"), - Name: PtrString("test"), + CapacityProviderArn: new("arn:aws:ecs:eu-west-2:052392120703:capacity-provider/test"), + Name: new("test"), Status: types.CapacityProviderStatusActive, AutoScalingGroupProvider: &types.AutoScalingGroupProvider{ - AutoScalingGroupArn: PtrString("arn:aws:autoscaling:eu-west-2:052392120703:autoScalingGroup:9df90815-98c1-4136-a12a-90abef1c4e4e:autoScalingGroupName/ecs-test"), + AutoScalingGroupArn: new("arn:aws:autoscaling:eu-west-2:052392120703:autoScalingGroup:9df90815-98c1-4136-a12a-90abef1c4e4e:autoScalingGroupName/ecs-test"), ManagedScaling: &types.ManagedScaling{ Status: types.ManagedScalingStatusEnabled, - TargetCapacity: PtrInt32(80), - MinimumScalingStepSize: PtrInt32(1), - MaximumScalingStepSize: PtrInt32(10000), - InstanceWarmupPeriod: PtrInt32(300), + TargetCapacity: new(int32(80)), + MinimumScalingStepSize: new(int32(1)), + MaximumScalingStepSize: new(int32(10000)), + InstanceWarmupPeriod: new(int32(300)), }, ManagedTerminationProtection: types.ManagedTerminationProtectionDisabled, }, UpdateStatus: types.CapacityProviderUpdateStatusDeleteComplete, - UpdateStatusReason: PtrString("reason"), + UpdateStatusReason: new("reason"), }, }, }, @@ -76,22 +76,22 @@ func TestCapacityProviderOutputMapper(t *testing.T) { &ecs.DescribeCapacityProvidersOutput{ CapacityProviders: []types.CapacityProvider{ { - CapacityProviderArn: PtrString("arn:aws:ecs:eu-west-2:052392120703:capacity-provider/test"), - Name: PtrString("test"), + CapacityProviderArn: new("arn:aws:ecs:eu-west-2:052392120703:capacity-provider/test"), + Name: new("test"), Status: types.CapacityProviderStatusActive, AutoScalingGroupProvider: &types.AutoScalingGroupProvider{ - AutoScalingGroupArn: PtrString("arn:aws:autoscaling:eu-west-2:052392120703:autoScalingGroup:9df90815-98c1-4136-a12a-90abef1c4e4e:autoScalingGroupName/ecs-test"), + AutoScalingGroupArn: new("arn:aws:autoscaling:eu-west-2:052392120703:autoScalingGroup:9df90815-98c1-4136-a12a-90abef1c4e4e:autoScalingGroupName/ecs-test"), ManagedScaling: &types.ManagedScaling{ Status: types.ManagedScalingStatusEnabled, - TargetCapacity: PtrInt32(80), - MinimumScalingStepSize: PtrInt32(1), - MaximumScalingStepSize: PtrInt32(10000), - InstanceWarmupPeriod: PtrInt32(300), + TargetCapacity: new(int32(80)), + MinimumScalingStepSize: new(int32(1)), + MaximumScalingStepSize: new(int32(10000)), + InstanceWarmupPeriod: new(int32(300)), }, ManagedTerminationProtection: types.ManagedTerminationProtectionDisabled, }, UpdateStatus: types.CapacityProviderUpdateStatusDeleteComplete, - UpdateStatusReason: PtrString("reason"), + UpdateStatusReason: new("reason"), }, }, }, diff --git a/aws-source/adapters/ecs-cluster_test.go b/aws-source/adapters/ecs-cluster_test.go index 3ee97b51..316b1066 100644 --- a/aws-source/adapters/ecs-cluster_test.go +++ b/aws-source/adapters/ecs-cluster_test.go @@ -15,24 +15,24 @@ func (t *ecsTestClient) DescribeClusters(ctx context.Context, params *ecs.Descri return &ecs.DescribeClustersOutput{ Clusters: []types.Cluster{ { - ClusterArn: PtrString("arn:aws:ecs:eu-west-2:052392120703:cluster/default"), - ClusterName: PtrString("default"), - Status: PtrString("ACTIVE"), + ClusterArn: new("arn:aws:ecs:eu-west-2:052392120703:cluster/default"), + ClusterName: new("default"), + Status: new("ACTIVE"), RegisteredContainerInstancesCount: 0, RunningTasksCount: 1, PendingTasksCount: 0, ActiveServicesCount: 1, Statistics: []types.KeyValuePair{ { - Name: PtrString("key"), - Value: PtrString("value"), + Name: new("key"), + Value: new("value"), }, }, Tags: []types.Tag{}, Settings: []types.ClusterSetting{ { Name: types.ClusterSettingNameContainerInsights, - Value: PtrString("ENABLED"), + Value: new("ENABLED"), }, }, CapacityProviders: []string{ @@ -40,43 +40,43 @@ func (t *ecsTestClient) DescribeClusters(ctx context.Context, params *ecs.Descri }, DefaultCapacityProviderStrategy: []types.CapacityProviderStrategyItem{ { - CapacityProvider: PtrString("provider"), + CapacityProvider: new("provider"), Base: 10, Weight: 100, }, }, Attachments: []types.Attachment{ { - Id: PtrString("1c1f9cf4-461c-4072-aab2-e2dd346c53e1"), - Type: PtrString("as_policy"), - Status: PtrString("CREATED"), + Id: new("1c1f9cf4-461c-4072-aab2-e2dd346c53e1"), + Type: new("as_policy"), + Status: new("CREATED"), Details: []types.KeyValuePair{ { - Name: PtrString("capacityProviderName"), - Value: PtrString("test"), + Name: new("capacityProviderName"), + Value: new("test"), }, { - Name: PtrString("scalingPolicyName"), - Value: PtrString("ECSManagedAutoScalingPolicy-d2f110eb-20a6-4278-9c1c-47d98e21b1ed"), + Name: new("scalingPolicyName"), + Value: new("ECSManagedAutoScalingPolicy-d2f110eb-20a6-4278-9c1c-47d98e21b1ed"), }, }, }, }, - AttachmentsStatus: PtrString("UPDATE_COMPLETE"), + AttachmentsStatus: new("UPDATE_COMPLETE"), Configuration: &types.ClusterConfiguration{ ExecuteCommandConfiguration: &types.ExecuteCommandConfiguration{ - KmsKeyId: PtrString("id"), + KmsKeyId: new("id"), LogConfiguration: &types.ExecuteCommandLogConfiguration{ CloudWatchEncryptionEnabled: true, - CloudWatchLogGroupName: PtrString("cloud-watch-name"), - S3BucketName: PtrString("s3-name"), + CloudWatchLogGroupName: new("cloud-watch-name"), + S3BucketName: new("s3-name"), S3EncryptionEnabled: true, - S3KeyPrefix: PtrString("prod"), + S3KeyPrefix: new("prod"), }, }, }, ServiceConnectDefaults: &types.ClusterServiceConnectDefaults{ - Namespace: PtrString("prod"), + Namespace: new("prod"), }, }, }, diff --git a/aws-source/adapters/ecs-container-instance.go b/aws-source/adapters/ecs-container-instance.go index 021c1a3e..99255436 100644 --- a/aws-source/adapters/ecs-container-instance.go +++ b/aws-source/adapters/ecs-container-instance.go @@ -118,7 +118,7 @@ func NewECSContainerInstanceAdapter(client ECSClient, accountID string, region s Region: region, GetFunc: containerInstanceGetFunc, AdapterMetadata: containerInstanceAdapterMetadata, - cache: cache, + cache: cache, GetInputMapper: func(scope, query string) *ecs.DescribeContainerInstancesInput { // We are using a custom id of {clusterName}/{id} e.g. // ecs-template-ECSCluster-8nS0WOLbs3nZ/50e9bf71ed57450ca56293cc5a042886 @@ -144,7 +144,7 @@ func NewECSContainerInstanceAdapter(client ECSClient, accountID string, region s SearchInputMapper: func(scope, query string) (*ecs.ListContainerInstancesInput, error) { // Custom search by cluster return &ecs.ListContainerInstancesInput{ - Cluster: PtrString(query), + Cluster: new(query), }, nil }, ListFuncOutputMapper: containerInstanceListFuncOutputMapper, diff --git a/aws-source/adapters/ecs-container-instance_test.go b/aws-source/adapters/ecs-container-instance_test.go index 6a9b8632..bdc53b47 100644 --- a/aws-source/adapters/ecs-container-instance_test.go +++ b/aws-source/adapters/ecs-container-instance_test.go @@ -15,32 +15,32 @@ func (t *ecsTestClient) DescribeContainerInstances(ctx context.Context, params * return &ecs.DescribeContainerInstancesOutput{ ContainerInstances: []types.ContainerInstance{ { - ContainerInstanceArn: PtrString("arn:aws:ecs:eu-west-1:052392120703:container-instance/ecs-template-ECSCluster-8nS0WOLbs3nZ/50e9bf71ed57450ca56293cc5a042886"), - Ec2InstanceId: PtrString("i-0e778f25705bc0c84"), // link + ContainerInstanceArn: new("arn:aws:ecs:eu-west-1:052392120703:container-instance/ecs-template-ECSCluster-8nS0WOLbs3nZ/50e9bf71ed57450ca56293cc5a042886"), + Ec2InstanceId: new("i-0e778f25705bc0c84"), // link Version: 4, VersionInfo: &types.VersionInfo{ - AgentVersion: PtrString("1.47.0"), - AgentHash: PtrString("1489adfa"), - DockerVersion: PtrString("DockerVersion: 19.03.6-ce"), + AgentVersion: new("1.47.0"), + AgentHash: new("1489adfa"), + DockerVersion: new("DockerVersion: 19.03.6-ce"), }, RemainingResources: []types.Resource{ { - Name: PtrString("CPU"), - Type: PtrString("INTEGER"), + Name: new("CPU"), + Type: new("INTEGER"), DoubleValue: 0.0, LongValue: 0, IntegerValue: 2028, }, { - Name: PtrString("MEMORY"), - Type: PtrString("INTEGER"), + Name: new("MEMORY"), + Type: new("INTEGER"), DoubleValue: 0.0, LongValue: 0, IntegerValue: 7474, }, { - Name: PtrString("PORTS"), - Type: PtrString("STRINGSET"), + Name: new("PORTS"), + Type: new("STRINGSET"), DoubleValue: 0.0, LongValue: 0, IntegerValue: 0, @@ -53,8 +53,8 @@ func (t *ecsTestClient) DescribeContainerInstances(ctx context.Context, params * }, }, { - Name: PtrString("PORTS_UDP"), - Type: PtrString("STRINGSET"), + Name: new("PORTS_UDP"), + Type: new("STRINGSET"), DoubleValue: 0.0, LongValue: 0, IntegerValue: 0, @@ -63,22 +63,22 @@ func (t *ecsTestClient) DescribeContainerInstances(ctx context.Context, params * }, RegisteredResources: []types.Resource{ { - Name: PtrString("CPU"), - Type: PtrString("INTEGER"), + Name: new("CPU"), + Type: new("INTEGER"), DoubleValue: 0.0, LongValue: 0, IntegerValue: 2048, }, { - Name: PtrString("MEMORY"), - Type: PtrString("INTEGER"), + Name: new("MEMORY"), + Type: new("INTEGER"), DoubleValue: 0.0, LongValue: 0, IntegerValue: 7974, }, { - Name: PtrString("PORTS"), - Type: PtrString("STRINGSET"), + Name: new("PORTS"), + Type: new("STRINGSET"), DoubleValue: 0.0, LongValue: 0, IntegerValue: 0, @@ -91,223 +91,223 @@ func (t *ecsTestClient) DescribeContainerInstances(ctx context.Context, params * }, }, { - Name: PtrString("PORTS_UDP"), - Type: PtrString("STRINGSET"), + Name: new("PORTS_UDP"), + Type: new("STRINGSET"), DoubleValue: 0.0, LongValue: 0, IntegerValue: 0, StringSetValue: []string{}, }, }, - Status: PtrString("ACTIVE"), + Status: new("ACTIVE"), AgentConnected: true, RunningTasksCount: 1, PendingTasksCount: 0, Attributes: []types.Attribute{ { - Name: PtrString("ecs.capability.secrets.asm.environment-variables"), + Name: new("ecs.capability.secrets.asm.environment-variables"), }, { - Name: PtrString("ecs.capability.branch-cni-plugin-version"), - Value: PtrString("a21d3a41-"), + Name: new("ecs.capability.branch-cni-plugin-version"), + Value: new("a21d3a41-"), }, { - Name: PtrString("ecs.ami-id"), - Value: PtrString("ami-0c9ef930279337028"), + Name: new("ecs.ami-id"), + Value: new("ami-0c9ef930279337028"), }, { - Name: PtrString("ecs.capability.secrets.asm.bootstrap.log-driver"), + Name: new("ecs.capability.secrets.asm.bootstrap.log-driver"), }, { - Name: PtrString("ecs.capability.task-eia.optimized-cpu"), + Name: new("ecs.capability.task-eia.optimized-cpu"), }, { - Name: PtrString("com.amazonaws.ecs.capability.logging-driver.none"), + Name: new("com.amazonaws.ecs.capability.logging-driver.none"), }, { - Name: PtrString("ecs.capability.ecr-endpoint"), + Name: new("ecs.capability.ecr-endpoint"), }, { - Name: PtrString("ecs.capability.docker-plugin.local"), + Name: new("ecs.capability.docker-plugin.local"), }, { - Name: PtrString("ecs.capability.task-cpu-mem-limit"), + Name: new("ecs.capability.task-cpu-mem-limit"), }, { - Name: PtrString("ecs.capability.secrets.ssm.bootstrap.log-driver"), + Name: new("ecs.capability.secrets.ssm.bootstrap.log-driver"), }, { - Name: PtrString("ecs.capability.efsAuth"), + Name: new("ecs.capability.efsAuth"), }, { - Name: PtrString("ecs.capability.full-sync"), + Name: new("ecs.capability.full-sync"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.30"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.30"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.31"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.31"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.32"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.32"), }, { - Name: PtrString("com.amazonaws.ecs.capability.logging-driver.fluentd"), + Name: new("com.amazonaws.ecs.capability.logging-driver.fluentd"), }, { - Name: PtrString("ecs.capability.firelens.options.config.file"), + Name: new("ecs.capability.firelens.options.config.file"), }, { - Name: PtrString("ecs.availability-zone"), - Value: PtrString("eu-west-1a"), + Name: new("ecs.availability-zone"), + Value: new("eu-west-1a"), }, { - Name: PtrString("ecs.capability.aws-appmesh"), + Name: new("ecs.capability.aws-appmesh"), }, { - Name: PtrString("com.amazonaws.ecs.capability.logging-driver.awslogs"), + Name: new("com.amazonaws.ecs.capability.logging-driver.awslogs"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.24"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.24"), }, { - Name: PtrString("ecs.capability.task-eni-trunking"), + Name: new("ecs.capability.task-eni-trunking"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.25"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.25"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.26"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.26"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.27"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.27"), }, { - Name: PtrString("com.amazonaws.ecs.capability.privileged-container"), + Name: new("com.amazonaws.ecs.capability.privileged-container"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.28"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.28"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.29"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.29"), }, { - Name: PtrString("ecs.cpu-architecture"), - Value: PtrString("x86_64"), + Name: new("ecs.cpu-architecture"), + Value: new("x86_64"), }, { - Name: PtrString("com.amazonaws.ecs.capability.ecr-auth"), + Name: new("com.amazonaws.ecs.capability.ecr-auth"), }, { - Name: PtrString("ecs.capability.firelens.fluentbit"), + Name: new("ecs.capability.firelens.fluentbit"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.20"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.20"), }, { - Name: PtrString("ecs.os-type"), - Value: PtrString("linux"), + Name: new("ecs.os-type"), + Value: new("linux"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.21"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.21"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.22"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.22"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.23"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.23"), }, { - Name: PtrString("ecs.capability.task-eia"), + Name: new("ecs.capability.task-eia"), }, { - Name: PtrString("ecs.capability.private-registry-authentication.secretsmanager"), + Name: new("ecs.capability.private-registry-authentication.secretsmanager"), }, { - Name: PtrString("com.amazonaws.ecs.capability.logging-driver.syslog"), + Name: new("com.amazonaws.ecs.capability.logging-driver.syslog"), }, { - Name: PtrString("com.amazonaws.ecs.capability.logging-driver.awsfirelens"), + Name: new("com.amazonaws.ecs.capability.logging-driver.awsfirelens"), }, { - Name: PtrString("ecs.capability.firelens.options.config.s3"), + Name: new("ecs.capability.firelens.options.config.s3"), }, { - Name: PtrString("com.amazonaws.ecs.capability.logging-driver.json-file"), + Name: new("com.amazonaws.ecs.capability.logging-driver.json-file"), }, { - Name: PtrString("ecs.capability.execution-role-awslogs"), + Name: new("ecs.capability.execution-role-awslogs"), }, { - Name: PtrString("ecs.vpc-id"), - Value: PtrString("vpc-0e120717a7263de70"), + Name: new("ecs.vpc-id"), + Value: new("vpc-0e120717a7263de70"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.17"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.17"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.18"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.18"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.19"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.19"), }, { - Name: PtrString("ecs.capability.docker-plugin.amazon-ecs-volume-plugin"), + Name: new("ecs.capability.docker-plugin.amazon-ecs-volume-plugin"), }, { - Name: PtrString("ecs.capability.task-eni"), + Name: new("ecs.capability.task-eni"), }, { - Name: PtrString("ecs.capability.firelens.fluentd"), + Name: new("ecs.capability.firelens.fluentd"), }, { - Name: PtrString("ecs.capability.efs"), + Name: new("ecs.capability.efs"), }, { - Name: PtrString("ecs.capability.execution-role-ecr-pull"), + Name: new("ecs.capability.execution-role-ecr-pull"), }, { - Name: PtrString("ecs.capability.task-eni.ipv6"), + Name: new("ecs.capability.task-eni.ipv6"), }, { - Name: PtrString("ecs.capability.container-health-check"), + Name: new("ecs.capability.container-health-check"), }, { - Name: PtrString("ecs.subnet-id"), - Value: PtrString("subnet-0bfdb717a234c01b3"), + Name: new("ecs.subnet-id"), + Value: new("subnet-0bfdb717a234c01b3"), }, { - Name: PtrString("ecs.instance-type"), - Value: PtrString("t2.large"), + Name: new("ecs.instance-type"), + Value: new("t2.large"), }, { - Name: PtrString("com.amazonaws.ecs.capability.task-iam-role-network-host"), + Name: new("com.amazonaws.ecs.capability.task-iam-role-network-host"), }, { - Name: PtrString("ecs.capability.container-ordering"), + Name: new("ecs.capability.container-ordering"), }, { - Name: PtrString("ecs.capability.cni-plugin-version"), - Value: PtrString("55b2ae77-2020.09.0"), + Name: new("ecs.capability.cni-plugin-version"), + Value: new("55b2ae77-2020.09.0"), }, { - Name: PtrString("ecs.capability.env-files.s3"), + Name: new("ecs.capability.env-files.s3"), }, { - Name: PtrString("ecs.capability.pid-ipc-namespace-sharing"), + Name: new("ecs.capability.pid-ipc-namespace-sharing"), }, { - Name: PtrString("ecs.capability.secrets.ssm.environment-variables"), + Name: new("ecs.capability.secrets.ssm.environment-variables"), }, { - Name: PtrString("com.amazonaws.ecs.capability.task-iam-role"), + Name: new("com.amazonaws.ecs.capability.task-iam-role"), }, }, - RegisteredAt: PtrTime(time.Now()), + RegisteredAt: new(time.Now()), Attachments: []types.Attachment{}, // There is probably an opportunity for some links here but I don't have example data Tags: []types.Tag{}, AgentUpdateStatus: types.AgentUpdateStatusFailed, - CapacityProviderName: PtrString("name"), + CapacityProviderName: new("name"), HealthStatus: &types.ContainerInstanceHealthStatus{ OverallStatus: types.InstanceHealthCheckStateImpaired, }, diff --git a/aws-source/adapters/ecs-service.go b/aws-source/adapters/ecs-service.go index fcbbc3ff..66cf2fb8 100644 --- a/aws-source/adapters/ecs-service.go +++ b/aws-source/adapters/ecs-service.go @@ -304,7 +304,7 @@ func NewECSServiceAdapter(client ECSClient, accountID string, region string, cac GetFunc: serviceGetFunc, DisableList: true, AdapterMetadata: ecsServiceAdapterMetadata, - cache: cache, + cache: cache, GetInputMapper: func(scope, query string) *ecs.DescribeServicesInput { // We are using a custom id of {clusterName}/{id} e.g. // ecs-template-ECSCluster-8nS0WOLbs3nZ/ecs-template-service-i0mQKzkhDI2C @@ -329,7 +329,7 @@ func NewECSServiceAdapter(client ECSClient, accountID string, region string, cac SearchInputMapper: func(scope, query string) (*ecs.ListServicesInput, error) { // Custom search by cluster return &ecs.ListServicesInput{ - Cluster: PtrString(query), + Cluster: new(query), }, nil }, ListFuncOutputMapper: serviceListFuncOutputMapper, diff --git a/aws-source/adapters/ecs-service_test.go b/aws-source/adapters/ecs-service_test.go index b7e6683d..a6921d25 100644 --- a/aws-source/adapters/ecs-service_test.go +++ b/aws-source/adapters/ecs-service_test.go @@ -16,37 +16,37 @@ func (t *ecsTestClient) DescribeServices(ctx context.Context, params *ecs.Descri Failures: []types.Failure{}, Services: []types.Service{ { - ServiceArn: PtrString("arn:aws:ecs:eu-west-1:052392120703:service/ecs-template-ECSCluster-8nS0WOLbs3nZ/ecs-template-service-i0mQKzkhDI2C"), - ServiceName: PtrString("ecs-template-service-i0mQKzkhDI2C"), - ClusterArn: PtrString("arn:aws:ecs:eu-west-1:052392120703:cluster/ecs-template-ECSCluster-8nS0WOLbs3nZ"), // link + ServiceArn: new("arn:aws:ecs:eu-west-1:052392120703:service/ecs-template-ECSCluster-8nS0WOLbs3nZ/ecs-template-service-i0mQKzkhDI2C"), + ServiceName: new("ecs-template-service-i0mQKzkhDI2C"), + ClusterArn: new("arn:aws:ecs:eu-west-1:052392120703:cluster/ecs-template-ECSCluster-8nS0WOLbs3nZ"), // link LoadBalancers: []types.LoadBalancer{ { - TargetGroupArn: PtrString("arn:aws:elasticloadbalancing:eu-west-1:052392120703:targetgroup/ECSTG/0c44b1cdb3437902"), // link - ContainerName: PtrString("simple-app"), - ContainerPort: PtrInt32(80), + TargetGroupArn: new("arn:aws:elasticloadbalancing:eu-west-1:052392120703:targetgroup/ECSTG/0c44b1cdb3437902"), // link + ContainerName: new("simple-app"), + ContainerPort: new(int32(80)), }, }, ServiceRegistries: []types.ServiceRegistry{ { - ContainerName: PtrString("name"), - ContainerPort: PtrInt32(80), - Port: PtrInt32(80), - RegistryArn: PtrString("arn:aws:service:region:account:type:name"), // link + ContainerName: new("name"), + ContainerPort: new(int32(80)), + Port: new(int32(80)), + RegistryArn: new("arn:aws:service:region:account:type:name"), // link }, }, - Status: PtrString("ACTIVE"), + Status: new("ACTIVE"), DesiredCount: 1, RunningCount: 1, PendingCount: 0, LaunchType: types.LaunchTypeEc2, - TaskDefinition: PtrString("arn:aws:ecs:eu-west-1:052392120703:task-definition/ecs-template-ecs-demo-app:1"), // link + TaskDefinition: new("arn:aws:ecs:eu-west-1:052392120703:task-definition/ecs-template-ecs-demo-app:1"), // link DeploymentConfiguration: &types.DeploymentConfiguration{ DeploymentCircuitBreaker: &types.DeploymentCircuitBreaker{ Enable: false, Rollback: false, }, - MaximumPercent: PtrInt32(200), - MinimumHealthyPercent: PtrInt32(100), + MaximumPercent: new(int32(200)), + MinimumHealthyPercent: new(int32(100)), Alarms: &types.DeploymentAlarms{ AlarmNames: []string{ "foo", @@ -57,21 +57,21 @@ func (t *ecsTestClient) DescribeServices(ctx context.Context, params *ecs.Descri }, Deployments: []types.Deployment{ { - Id: PtrString("ecs-svc/6893472562508357546"), - Status: PtrString("PRIMARY"), - TaskDefinition: PtrString("arn:aws:ecs:eu-west-1:052392120703:task-definition/ecs-template-ecs-demo-app:1"), // link + Id: new("ecs-svc/6893472562508357546"), + Status: new("PRIMARY"), + TaskDefinition: new("arn:aws:ecs:eu-west-1:052392120703:task-definition/ecs-template-ecs-demo-app:1"), // link DesiredCount: 1, PendingCount: 0, RunningCount: 1, FailedTasks: 0, - CreatedAt: PtrTime(time.Now()), - UpdatedAt: PtrTime(time.Now()), + CreatedAt: new(time.Now()), + UpdatedAt: new(time.Now()), LaunchType: types.LaunchTypeEc2, RolloutState: types.DeploymentRolloutStateCompleted, - RolloutStateReason: PtrString("ECS deployment ecs-svc/6893472562508357546 completed."), + RolloutStateReason: new("ECS deployment ecs-svc/6893472562508357546 completed."), CapacityProviderStrategy: []types.CapacityProviderStrategyItem{ { - CapacityProvider: PtrString("provider"), // link + CapacityProvider: new("provider"), // link Base: 10, Weight: 10, }, @@ -87,8 +87,8 @@ func (t *ecsTestClient) DescribeServices(ctx context.Context, params *ecs.Descri }, }, }, - PlatformFamily: PtrString("foo"), - PlatformVersion: PtrString("LATEST"), + PlatformFamily: new("foo"), + PlatformVersion: new("LATEST"), ServiceConnectConfiguration: &types.ServiceConnectConfiguration{ Enabled: true, LogConfiguration: &types.LogConfiguration{ @@ -96,19 +96,19 @@ func (t *ecsTestClient) DescribeServices(ctx context.Context, params *ecs.Descri Options: map[string]string{}, SecretOptions: []types.Secret{ { - Name: PtrString("something"), - ValueFrom: PtrString("somewhere"), + Name: new("something"), + ValueFrom: new("somewhere"), }, }, }, - Namespace: PtrString("namespace"), + Namespace: new("namespace"), Services: []types.ServiceConnectService{ { - PortName: PtrString("http"), + PortName: new("http"), ClientAliases: []types.ServiceConnectClientAlias{ { - Port: PtrInt32(80), - DnsName: PtrString("www.foo.com"), // link + Port: new(int32(80)), + DnsName: new("www.foo.com"), // link }, }, }, @@ -116,65 +116,65 @@ func (t *ecsTestClient) DescribeServices(ctx context.Context, params *ecs.Descri }, ServiceConnectResources: []types.ServiceConnectServiceResource{ { - DiscoveryArn: PtrString("arn:aws:service:region:account:layer:name:version"), // link - DiscoveryName: PtrString("name"), + DiscoveryArn: new("arn:aws:service:region:account:layer:name:version"), // link + DiscoveryName: new("name"), }, }, }, }, - RoleArn: PtrString("arn:aws:iam::052392120703:role/ecs-template-ECSServiceRole-1IL5CNMR1600J"), + RoleArn: new("arn:aws:iam::052392120703:role/ecs-template-ECSServiceRole-1IL5CNMR1600J"), Events: []types.ServiceEvent{ { - Id: PtrString("a727ef2a-8a38-4746-905e-b529c952edee"), - CreatedAt: PtrTime(time.Now()), - Message: PtrString("(service ecs-template-service-i0mQKzkhDI2C) has reached a steady state."), + Id: new("a727ef2a-8a38-4746-905e-b529c952edee"), + CreatedAt: new(time.Now()), + Message: new("(service ecs-template-service-i0mQKzkhDI2C) has reached a steady state."), }, { - Id: PtrString("69489991-f8ee-42a2-94f2-db8ffeda1ee7"), - CreatedAt: PtrTime(time.Now()), - Message: PtrString("(service ecs-template-service-i0mQKzkhDI2C) (deployment ecs-svc/6893472562508357546) deployment completed."), + Id: new("69489991-f8ee-42a2-94f2-db8ffeda1ee7"), + CreatedAt: new(time.Now()), + Message: new("(service ecs-template-service-i0mQKzkhDI2C) (deployment ecs-svc/6893472562508357546) deployment completed."), }, { - Id: PtrString("9ce65c4b-2993-477d-aa83-dbe98988f90b"), - CreatedAt: PtrTime(time.Now()), - Message: PtrString("(service ecs-template-service-i0mQKzkhDI2C) registered 1 targets in (target-group arn:aws:elasticloadbalancing:eu-west-1:052392120703:targetgroup/ECSTG/0c44b1cdb3437902)"), + Id: new("9ce65c4b-2993-477d-aa83-dbe98988f90b"), + CreatedAt: new(time.Now()), + Message: new("(service ecs-template-service-i0mQKzkhDI2C) registered 1 targets in (target-group arn:aws:elasticloadbalancing:eu-west-1:052392120703:targetgroup/ECSTG/0c44b1cdb3437902)"), }, { - Id: PtrString("753e988a-9fb9-4907-b801-5f67369bc0de"), - CreatedAt: PtrTime(time.Now()), - Message: PtrString("(service ecs-template-service-i0mQKzkhDI2C) has started 1 tasks: (task 53074e0156204f30a3cea97e7bf32d31)."), + Id: new("753e988a-9fb9-4907-b801-5f67369bc0de"), + CreatedAt: new(time.Now()), + Message: new("(service ecs-template-service-i0mQKzkhDI2C) has started 1 tasks: (task 53074e0156204f30a3cea97e7bf32d31)."), }, { - Id: PtrString("deb2400b-a776-4ebe-8c97-f94feef2b780"), - CreatedAt: PtrTime(time.Now()), - Message: PtrString("(service ecs-template-service-i0mQKzkhDI2C) was unable to place a task because no container instance met all of its requirements. Reason: No Container Instances were found in your cluster. For more information, see the Troubleshooting section of the Amazon ECS Developer Guide."), + Id: new("deb2400b-a776-4ebe-8c97-f94feef2b780"), + CreatedAt: new(time.Now()), + Message: new("(service ecs-template-service-i0mQKzkhDI2C) was unable to place a task because no container instance met all of its requirements. Reason: No Container Instances were found in your cluster. For more information, see the Troubleshooting section of the Amazon ECS Developer Guide."), }, }, - CreatedAt: PtrTime(time.Now()), + CreatedAt: new(time.Now()), PlacementConstraints: []types.PlacementConstraint{ { - Expression: PtrString("expression"), + Expression: new("expression"), Type: types.PlacementConstraintTypeDistinctInstance, }, }, PlacementStrategy: []types.PlacementStrategy{ { - Field: PtrString("field"), + Field: new("field"), Type: types.PlacementStrategyTypeSpread, }, }, - HealthCheckGracePeriodSeconds: PtrInt32(0), + HealthCheckGracePeriodSeconds: new(int32(0)), SchedulingStrategy: types.SchedulingStrategyReplica, DeploymentController: &types.DeploymentController{ Type: types.DeploymentControllerTypeEcs, }, - CreatedBy: PtrString("arn:aws:iam::052392120703:role/aws-reserved/sso.amazonaws.com/eu-west-2/AWSReservedSSO_AWSAdministratorAccess_c1c3c9c54821c68a"), + CreatedBy: new("arn:aws:iam::052392120703:role/aws-reserved/sso.amazonaws.com/eu-west-2/AWSReservedSSO_AWSAdministratorAccess_c1c3c9c54821c68a"), EnableECSManagedTags: false, PropagateTags: types.PropagateTagsNone, EnableExecuteCommand: false, CapacityProviderStrategy: []types.CapacityProviderStrategyItem{ { - CapacityProvider: PtrString("provider"), + CapacityProvider: new("provider"), Base: 10, Weight: 10, }, @@ -190,15 +190,15 @@ func (t *ecsTestClient) DescribeServices(ctx context.Context, params *ecs.Descri }, }, }, - PlatformFamily: PtrString("family"), - PlatformVersion: PtrString("LATEST"), + PlatformFamily: new("family"), + PlatformVersion: new("LATEST"), Tags: []types.Tag{}, TaskSets: []types.TaskSet{ // This seems to be able to return the *entire* task set, // which is redundant info. We should remove everything // other than the IDs { - Id: PtrString("id"), // link, then remove + Id: new("id"), // link, then remove }, }, }, diff --git a/aws-source/adapters/ecs-task-definition.go b/aws-source/adapters/ecs-task-definition.go index 41e691f6..5756feae 100644 --- a/aws-source/adapters/ecs-task-definition.go +++ b/aws-source/adapters/ecs-task-definition.go @@ -159,12 +159,12 @@ func NewECSTaskDefinitionAdapter(client ECSClient, accountID string, region stri GetFunc: taskDefinitionGetFunc, ListInput: &ecs.ListTaskDefinitionsInput{}, AdapterMetadata: taskDefinitionAdapterMetadata, - cache: cache, + cache: cache, GetInputMapper: func(scope, query string) *ecs.DescribeTaskDefinitionInput { // AWS actually supports "family:revision" format as an input here // so we can just push it in directly return &ecs.DescribeTaskDefinitionInput{ - TaskDefinition: PtrString(query), + TaskDefinition: new(query), } }, ListFuncPaginatorBuilder: func(client ECSClient, input *ecs.ListTaskDefinitionsInput) Paginator[*ecs.ListTaskDefinitionsOutput, *ecs.Options] { @@ -176,7 +176,7 @@ func NewECSTaskDefinitionAdapter(client ECSClient, accountID string, region stri for _, arn := range output.TaskDefinitionArns { if a, err := ParseARN(arn); err == nil { getInputs = append(getInputs, &ecs.DescribeTaskDefinitionInput{ - TaskDefinition: PtrString(a.ResourceID()), + TaskDefinition: new(a.ResourceID()), }) } } diff --git a/aws-source/adapters/ecs-task-definition_test.go b/aws-source/adapters/ecs-task-definition_test.go index 4b7ce3cb..e4b772f2 100644 --- a/aws-source/adapters/ecs-task-definition_test.go +++ b/aws-source/adapters/ecs-task-definition_test.go @@ -14,60 +14,60 @@ import ( func (t *ecsTestClient) DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) { return &ecs.DescribeTaskDefinitionOutput{ TaskDefinition: &types.TaskDefinition{ - TaskDefinitionArn: PtrString("arn:aws:ecs:eu-west-1:052392120703:task-definition/ecs-template-ecs-demo-app:1"), + TaskDefinitionArn: new("arn:aws:ecs:eu-west-1:052392120703:task-definition/ecs-template-ecs-demo-app:1"), ContainerDefinitions: []types.ContainerDefinition{ { - Name: PtrString("simple-app"), - Image: PtrString("httpd:2.4"), + Name: new("simple-app"), + Image: new("httpd:2.4"), Cpu: 10, - Memory: PtrInt32(300), + Memory: new(int32(300)), Links: []string{}, PortMappings: []types.PortMapping{ { - ContainerPort: PtrInt32(80), - HostPort: PtrInt32(0), + ContainerPort: new(int32(80)), + HostPort: new(int32(0)), Protocol: types.TransportProtocolTcp, AppProtocol: types.ApplicationProtocolHttp, }, }, - Essential: PtrBool(true), + Essential: new(true), EntryPoint: []string{}, Command: []string{}, Environment: []types.KeyValuePair{ { - Name: PtrString("DATABASE_SERVER"), - Value: PtrString("database01.my-company.com"), + Name: new("DATABASE_SERVER"), + Value: new("database01.my-company.com"), }, }, EnvironmentFiles: []types.EnvironmentFile{}, MountPoints: []types.MountPoint{ { - SourceVolume: PtrString("my-vol"), - ContainerPath: PtrString("/usr/local/apache2/htdocs"), - ReadOnly: PtrBool(false), + SourceVolume: new("my-vol"), + ContainerPath: new("/usr/local/apache2/htdocs"), + ReadOnly: new(false), }, }, VolumesFrom: []types.VolumeFrom{ { - SourceContainer: PtrString("container"), + SourceContainer: new("container"), }, }, Secrets: []types.Secret{ { - Name: PtrString("secrets-manager"), - ValueFrom: PtrString("arn:aws:secretsmanager:us-west-2:123456789012:secret:my-path/my-secret-name-1a2b3c"), // link + Name: new("secrets-manager"), + ValueFrom: new("arn:aws:secretsmanager:us-west-2:123456789012:secret:my-path/my-secret-name-1a2b3c"), // link }, { - Name: PtrString("ssm"), - ValueFrom: PtrString("arn:aws:ssm:us-east-2:123456789012:parameter/prod-123"), // link + Name: new("ssm"), + ValueFrom: new("arn:aws:ssm:us-east-2:123456789012:parameter/prod-123"), // link }, }, DnsServers: []string{}, DnsSearchDomains: []string{}, ExtraHosts: []types.HostEntry{ { - Hostname: PtrString("host"), - IpAddress: PtrString("127.0.0.1"), + Hostname: new("host"), + IpAddress: new("127.0.0.1"), }, }, DockerSecurityOptions: []string{}, @@ -82,43 +82,43 @@ func (t *ecsTestClient) DescribeTaskDefinition(ctx context.Context, params *ecs. }, SecretOptions: []types.Secret{ { - Name: PtrString("secrets-manager"), - ValueFrom: PtrString("arn:aws:secretsmanager:us-west-2:123456789012:secret:my-path/my-secret-name-1a2b3c"), // link + Name: new("secrets-manager"), + ValueFrom: new("arn:aws:secretsmanager:us-west-2:123456789012:secret:my-path/my-secret-name-1a2b3c"), // link }, { - Name: PtrString("ssm"), - ValueFrom: PtrString("arn:aws:ssm:us-east-2:123456789012:parameter/prod-123"), // link + Name: new("ssm"), + ValueFrom: new("arn:aws:ssm:us-east-2:123456789012:parameter/prod-123"), // link }, }, }, SystemControls: []types.SystemControl{}, DependsOn: []types.ContainerDependency{}, - DisableNetworking: PtrBool(false), + DisableNetworking: new(false), FirelensConfiguration: &types.FirelensConfiguration{ Type: types.FirelensConfigurationTypeFluentd, Options: map[string]string{}, }, HealthCheck: &types.HealthCheck{}, - Hostname: PtrString("hostname"), - Interactive: PtrBool(false), + Hostname: new("hostname"), + Interactive: new(false), LinuxParameters: &types.LinuxParameters{}, - MemoryReservation: PtrInt32(100), - Privileged: PtrBool(false), - PseudoTerminal: PtrBool(false), - ReadonlyRootFilesystem: PtrBool(false), + MemoryReservation: new(int32(100)), + Privileged: new(false), + PseudoTerminal: new(false), + ReadonlyRootFilesystem: new(false), RepositoryCredentials: &types.RepositoryCredentials{}, // Skipping the link here for now, if you need it, add it in a PR ResourceRequirements: []types.ResourceRequirement{}, - StartTimeout: PtrInt32(1), - StopTimeout: PtrInt32(1), - User: PtrString("foo"), - WorkingDirectory: PtrString("/"), + StartTimeout: new(int32(1)), + StopTimeout: new(int32(1)), + User: new("foo"), + WorkingDirectory: new("/"), }, { - Name: PtrString("busybox"), - Image: PtrString("busybox"), + Name: new("busybox"), + Image: new("busybox"), Cpu: 10, - Memory: PtrInt32(200), - Essential: PtrBool(false), + Memory: new(int32(200)), + Essential: new(false), EntryPoint: []string{ "sh", "-c", @@ -128,7 +128,7 @@ func (t *ecsTestClient) DescribeTaskDefinition(ctx context.Context, params *ecs. }, VolumesFrom: []types.VolumeFrom{ { - SourceContainer: PtrString("simple-app"), + SourceContainer: new("simple-app"), }, }, DockerLabels: map[string]string{}, @@ -142,29 +142,29 @@ func (t *ecsTestClient) DescribeTaskDefinition(ctx context.Context, params *ecs. }, }, }, - Family: PtrString("ecs-template-ecs-demo-app"), + Family: new("ecs-template-ecs-demo-app"), Revision: 1, Volumes: []types.Volume{ { - Name: PtrString("my-vol"), + Name: new("my-vol"), Host: &types.HostVolumeProperties{ - SourcePath: PtrString("/"), + SourcePath: new("/"), }, }, }, Status: types.TaskDefinitionStatusActive, RequiresAttributes: []types.Attribute{ { - Name: PtrString("com.amazonaws.ecs.capability.logging-driver.awslogs"), + Name: new("com.amazonaws.ecs.capability.logging-driver.awslogs"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.19"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.19"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.17"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.17"), }, { - Name: PtrString("com.amazonaws.ecs.capability.docker-remote-api.1.18"), + Name: new("com.amazonaws.ecs.capability.docker-remote-api.1.18"), }, }, PlacementConstraints: []types.TaskDefinitionPlacementConstraint{}, @@ -172,17 +172,17 @@ func (t *ecsTestClient) DescribeTaskDefinition(ctx context.Context, params *ecs. "EXTERNAL", "EC2", }, - RegisteredAt: PtrTime(time.Now()), - RegisteredBy: PtrString("arn:aws:sts::052392120703:assumed-role/AWSReservedSSO_AWSAdministratorAccess_c1c3c9c54821c68a/dylan@overmind.tech"), - Cpu: PtrString("cpu"), - DeregisteredAt: PtrTime(time.Now()), + RegisteredAt: new(time.Now()), + RegisteredBy: new("arn:aws:sts::052392120703:assumed-role/AWSReservedSSO_AWSAdministratorAccess_c1c3c9c54821c68a/dylan@overmind.tech"), + Cpu: new("cpu"), + DeregisteredAt: new(time.Now()), EphemeralStorage: &types.EphemeralStorage{ SizeInGiB: 1, }, - ExecutionRoleArn: PtrString("arn:aws:iam:us-east-2:123456789012:role/foo"), // link + ExecutionRoleArn: new("arn:aws:iam:us-east-2:123456789012:role/foo"), // link InferenceAccelerators: []types.InferenceAccelerator{}, IpcMode: types.IpcModeHost, - Memory: PtrString("memory"), + Memory: new("memory"), NetworkMode: types.NetworkModeAwsvpc, PidMode: types.PidModeHost, ProxyConfiguration: nil, @@ -191,7 +191,7 @@ func (t *ecsTestClient) DescribeTaskDefinition(ctx context.Context, params *ecs. CpuArchitecture: types.CPUArchitectureX8664, OperatingSystemFamily: types.OSFamilyLinux, }, - TaskRoleArn: PtrString("arn:aws:iam:us-east-2:123456789012:role/bar"), // link + TaskRoleArn: new("arn:aws:iam:us-east-2:123456789012:role/bar"), // link }, }, nil } @@ -206,7 +206,7 @@ func (t *ecsTestClient) ListTaskDefinitions(context.Context, *ecs.ListTaskDefini func TestTaskDefinitionGetFunc(t *testing.T) { item, err := taskDefinitionGetFunc(context.Background(), &ecsTestClient{}, "foo", &ecs.DescribeTaskDefinitionInput{ - TaskDefinition: PtrString("ecs-template-ecs-demo-app:1"), + TaskDefinition: new("ecs-template-ecs-demo-app:1"), }) if err != nil { diff --git a/aws-source/adapters/ecs-task.go b/aws-source/adapters/ecs-task.go index b301f1a7..69cf56ee 100644 --- a/aws-source/adapters/ecs-task.go +++ b/aws-source/adapters/ecs-task.go @@ -165,7 +165,7 @@ func taskGetInputMapper(scope, query string) *ecs.DescribeTasksInput { Tasks: []string{ sections[1], }, - Cluster: PtrString(sections[0]), + Cluster: new(sections[0]), Include: TaskIncludeFields, } } @@ -203,14 +203,14 @@ func NewECSTaskAdapter(client ECSClient, accountID string, region string, cache Region: region, GetFunc: taskGetFunc, AdapterMetadata: ecsTaskAdapterMetadata, - cache: cache, + cache: cache, ListInput: &ecs.ListTasksInput{}, GetInputMapper: taskGetInputMapper, DisableList: true, SearchInputMapper: func(scope, query string) (*ecs.ListTasksInput, error) { // Search by cluster return &ecs.ListTasksInput{ - Cluster: PtrString(query), + Cluster: new(query), }, nil }, ListFuncPaginatorBuilder: func(client ECSClient, input *ecs.ListTasksInput) Paginator[*ecs.ListTasksOutput, *ecs.Options] { diff --git a/aws-source/adapters/ecs-task_test.go b/aws-source/adapters/ecs-task_test.go index e0e8b1b0..9179758e 100644 --- a/aws-source/adapters/ecs-task_test.go +++ b/aws-source/adapters/ecs-task_test.go @@ -17,90 +17,90 @@ func (t *ecsTestClient) DescribeTasks(ctx context.Context, params *ecs.DescribeT { Attachments: []types.Attachment{ { - Id: PtrString("id"), // link? - Status: PtrString("OK"), - Type: PtrString("ElasticNetworkInterface"), + Id: new("id"), // link? + Status: new("OK"), + Type: new("ElasticNetworkInterface"), }, }, Attributes: []types.Attribute{ { - Name: PtrString("ecs.cpu-architecture"), - Value: PtrString("x86_64"), + Name: new("ecs.cpu-architecture"), + Value: new("x86_64"), }, }, - AvailabilityZone: PtrString("eu-west-1c"), - ClusterArn: PtrString("arn:aws:ecs:eu-west-1:052392120703:cluster/test-ECSCluster-Bt4SqcM3CURk"), // link + AvailabilityZone: new("eu-west-1c"), + ClusterArn: new("arn:aws:ecs:eu-west-1:052392120703:cluster/test-ECSCluster-Bt4SqcM3CURk"), // link Connectivity: types.ConnectivityConnected, - ConnectivityAt: PtrTime(time.Now()), - ContainerInstanceArn: PtrString("arn:aws:ecs:eu-west-1:052392120703:container-instance/test-ECSCluster-Bt4SqcM3CURk/4b5c1d7dbb6746b38ada1b97b1866f6a"), // link + ConnectivityAt: new(time.Now()), + ContainerInstanceArn: new("arn:aws:ecs:eu-west-1:052392120703:container-instance/test-ECSCluster-Bt4SqcM3CURk/4b5c1d7dbb6746b38ada1b97b1866f6a"), // link Containers: []types.Container{ { - ContainerArn: PtrString("arn:aws:ecs:eu-west-1:052392120703:container/test-ECSCluster-Bt4SqcM3CURk/2ffd7ed376c841bcb0e6795ddb6e72e2/39a3ede1-1b28-472e-967a-d87d691f65e0"), - TaskArn: PtrString("arn:aws:ecs:eu-west-1:052392120703:task/test-ECSCluster-Bt4SqcM3CURk/2ffd7ed376c841bcb0e6795ddb6e72e2"), - Name: PtrString("busybox"), - Image: PtrString("busybox"), - RuntimeId: PtrString("7c158f5c2711416cbb6e653ad90997346489c9722c59d1115ad2121dd040748e"), - LastStatus: PtrString("RUNNING"), + ContainerArn: new("arn:aws:ecs:eu-west-1:052392120703:container/test-ECSCluster-Bt4SqcM3CURk/2ffd7ed376c841bcb0e6795ddb6e72e2/39a3ede1-1b28-472e-967a-d87d691f65e0"), + TaskArn: new("arn:aws:ecs:eu-west-1:052392120703:task/test-ECSCluster-Bt4SqcM3CURk/2ffd7ed376c841bcb0e6795ddb6e72e2"), + Name: new("busybox"), + Image: new("busybox"), + RuntimeId: new("7c158f5c2711416cbb6e653ad90997346489c9722c59d1115ad2121dd040748e"), + LastStatus: new("RUNNING"), NetworkBindings: []types.NetworkBinding{}, NetworkInterfaces: []types.NetworkInterface{}, HealthStatus: types.HealthStatusUnknown, - Cpu: PtrString("10"), - Memory: PtrString("200"), + Cpu: new("10"), + Memory: new("200"), }, { - ContainerArn: PtrString("arn:aws:ecs:eu-west-1:052392120703:container/test-ECSCluster-Bt4SqcM3CURk/2ffd7ed376c841bcb0e6795ddb6e72e2/8f3db814-6b39-4cc0-9d0a-a7d5702175eb"), - TaskArn: PtrString("arn:aws:ecs:eu-west-1:052392120703:task/test-ECSCluster-Bt4SqcM3CURk/2ffd7ed376c841bcb0e6795ddb6e72e2"), - Name: PtrString("simple-app"), - Image: PtrString("httpd:2.4"), - RuntimeId: PtrString("7316b64efb397cececce7cc5f39c6d48ab454f904cc80009aef5ed01ebdb1333"), - LastStatus: PtrString("RUNNING"), + ContainerArn: new("arn:aws:ecs:eu-west-1:052392120703:container/test-ECSCluster-Bt4SqcM3CURk/2ffd7ed376c841bcb0e6795ddb6e72e2/8f3db814-6b39-4cc0-9d0a-a7d5702175eb"), + TaskArn: new("arn:aws:ecs:eu-west-1:052392120703:task/test-ECSCluster-Bt4SqcM3CURk/2ffd7ed376c841bcb0e6795ddb6e72e2"), + Name: new("simple-app"), + Image: new("httpd:2.4"), + RuntimeId: new("7316b64efb397cececce7cc5f39c6d48ab454f904cc80009aef5ed01ebdb1333"), + LastStatus: new("RUNNING"), NetworkBindings: []types.NetworkBinding{ { - BindIP: PtrString("0.0.0.0"), // Link? NetworkSocket? - ContainerPort: PtrInt32(80), - HostPort: PtrInt32(32768), + BindIP: new("0.0.0.0"), // Link? NetworkSocket? + ContainerPort: new(int32(80)), + HostPort: new(int32(32768)), Protocol: types.TransportProtocolTcp, }, }, NetworkInterfaces: []types.NetworkInterface{ { - AttachmentId: PtrString("attachmentId"), - Ipv6Address: PtrString("2001:db8:3333:4444:5555:6666:7777:8888"), // link - PrivateIpv4Address: PtrString("10.0.0.1"), // link + AttachmentId: new("attachmentId"), + Ipv6Address: new("2001:db8:3333:4444:5555:6666:7777:8888"), // link + PrivateIpv4Address: new("10.0.0.1"), // link }, }, HealthStatus: types.HealthStatusUnknown, - Cpu: PtrString("10"), - Memory: PtrString("300"), + Cpu: new("10"), + Memory: new("300"), }, }, - Cpu: PtrString("20"), - CreatedAt: PtrTime(time.Now()), - DesiredStatus: PtrString("RUNNING"), + Cpu: new("20"), + CreatedAt: new(time.Now()), + DesiredStatus: new("RUNNING"), EnableExecuteCommand: false, - Group: PtrString("service:test-service-lszmaXSqRKuF"), + Group: new("service:test-service-lszmaXSqRKuF"), HealthStatus: types.HealthStatusUnknown, - LastStatus: PtrString("RUNNING"), + LastStatus: new("RUNNING"), LaunchType: types.LaunchTypeEc2, - Memory: PtrString("500"), + Memory: new("500"), Overrides: &types.TaskOverride{ ContainerOverrides: []types.ContainerOverride{ { - Name: PtrString("busybox"), + Name: new("busybox"), }, { - Name: PtrString("simple-app"), + Name: new("simple-app"), }, }, InferenceAcceleratorOverrides: []types.InferenceAcceleratorOverride{}, }, - PullStartedAt: PtrTime(time.Now()), - PullStoppedAt: PtrTime(time.Now()), - StartedAt: PtrTime(time.Now()), - StartedBy: PtrString("ecs-svc/0710912874193920929"), + PullStartedAt: new(time.Now()), + PullStoppedAt: new(time.Now()), + StartedAt: new(time.Now()), + StartedBy: new("ecs-svc/0710912874193920929"), Tags: []types.Tag{}, - TaskArn: PtrString("arn:aws:ecs:eu-west-1:052392120703:task/test-ECSCluster-Bt4SqcM3CURk/2ffd7ed376c841bcb0e6795ddb6e72e2"), - TaskDefinitionArn: PtrString("arn:aws:ecs:eu-west-1:052392120703:task-definition/test-ecs-demo-app:1"), // link + TaskArn: new("arn:aws:ecs:eu-west-1:052392120703:task/test-ECSCluster-Bt4SqcM3CURk/2ffd7ed376c841bcb0e6795ddb6e72e2"), + TaskDefinitionArn: new("arn:aws:ecs:eu-west-1:052392120703:task-definition/test-ecs-demo-app:1"), // link Version: 3, EphemeralStorage: &types.EphemeralStorage{ SizeInGiB: 1, diff --git a/aws-source/adapters/efs-access-point_test.go b/aws-source/adapters/efs-access-point_test.go index 2a47b2a6..0c29c7c9 100644 --- a/aws-source/adapters/efs-access-point_test.go +++ b/aws-source/adapters/efs-access-point_test.go @@ -15,32 +15,32 @@ func TestAccessPointOutputMapper(t *testing.T) { output := &efs.DescribeAccessPointsOutput{ AccessPoints: []types.AccessPointDescription{ { - AccessPointArn: PtrString("arn:aws:elasticfilesystem:eu-west-2:944651592624:access-point/fsap-073b1534eafbc5ee2"), - AccessPointId: PtrString("fsap-073b1534eafbc5ee2"), - ClientToken: PtrString("pvc-66e4418c-edf5-4a0e-9834-5945598d51fe"), - FileSystemId: PtrString("fs-0c6f2f41e957f42a9"), + AccessPointArn: new("arn:aws:elasticfilesystem:eu-west-2:944651592624:access-point/fsap-073b1534eafbc5ee2"), + AccessPointId: new("fsap-073b1534eafbc5ee2"), + ClientToken: new("pvc-66e4418c-edf5-4a0e-9834-5945598d51fe"), + FileSystemId: new("fs-0c6f2f41e957f42a9"), LifeCycleState: types.LifeCycleStateAvailable, - Name: PtrString("example access point"), - OwnerId: PtrString("944651592624"), + Name: new("example access point"), + OwnerId: new("944651592624"), PosixUser: &types.PosixUser{ - Gid: PtrInt64(1000), - Uid: PtrInt64(1000), + Gid: new(int64(1000)), + Uid: new(int64(1000)), SecondaryGids: []int64{ 1002, }, }, RootDirectory: &types.RootDirectory{ CreationInfo: &types.CreationInfo{ - OwnerGid: PtrInt64(1000), - OwnerUid: PtrInt64(1000), - Permissions: PtrString("700"), + OwnerGid: new(int64(1000)), + OwnerUid: new(int64(1000)), + Permissions: new("700"), }, - Path: PtrString("/etc/foo"), + Path: new("/etc/foo"), }, Tags: []types.Tag{ { - Key: PtrString("Name"), - Value: PtrString("example access point"), + Key: new("Name"), + Value: new("example access point"), }, }, }, diff --git a/aws-source/adapters/efs-backup-policy_test.go b/aws-source/adapters/efs-backup-policy_test.go index 7cb04558..560bb827 100644 --- a/aws-source/adapters/efs-backup-policy_test.go +++ b/aws-source/adapters/efs-backup-policy_test.go @@ -16,7 +16,7 @@ func TestBackupPolicyOutputMapper(t *testing.T) { } items, err := BackupPolicyOutputMapper(context.Background(), nil, "foo", &efs.DescribeBackupPolicyInput{ - FileSystemId: PtrString("fs-1234"), + FileSystemId: new("fs-1234"), }, output) if err != nil { diff --git a/aws-source/adapters/efs-file-system_test.go b/aws-source/adapters/efs-file-system_test.go index 6b0831dd..11fd47e8 100644 --- a/aws-source/adapters/efs-file-system_test.go +++ b/aws-source/adapters/efs-file-system_test.go @@ -15,32 +15,32 @@ func TestFileSystemOutputMapper(t *testing.T) { output := &efs.DescribeFileSystemsOutput{ FileSystems: []types.FileSystemDescription{ { - CreationTime: PtrTime(time.Now()), - CreationToken: PtrString("TOKEN"), - FileSystemId: PtrString("fs-1231123123"), + CreationTime: new(time.Now()), + CreationToken: new("TOKEN"), + FileSystemId: new("fs-1231123123"), LifeCycleState: types.LifeCycleStateAvailable, NumberOfMountTargets: 10, - OwnerId: PtrString("944651592624"), + OwnerId: new("944651592624"), PerformanceMode: types.PerformanceModeGeneralPurpose, SizeInBytes: &types.FileSystemSize{ Value: 1024, - Timestamp: PtrTime(time.Now()), - ValueInIA: PtrInt64(2048), - ValueInStandard: PtrInt64(128), + Timestamp: new(time.Now()), + ValueInIA: new(int64(2048)), + ValueInStandard: new(int64(128)), }, Tags: []types.Tag{ { - Key: PtrString("foo"), - Value: PtrString("bar"), + Key: new("foo"), + Value: new("bar"), }, }, - AvailabilityZoneId: PtrString("use1-az1"), - AvailabilityZoneName: PtrString("us-east-1"), - Encrypted: PtrBool(true), - FileSystemArn: PtrString("arn:aws:elasticfilesystem:eu-west-2:944651592624:file-system/fs-0c6f2f41e957f42a9"), - KmsKeyId: PtrString("arn:aws:kms:eu-west-2:944651592624:key/be76a6fa-d307-41c2-a4e3-cbfba2440747"), - Name: PtrString("test"), - ProvisionedThroughputInMibps: PtrFloat64(64), + AvailabilityZoneId: new("use1-az1"), + AvailabilityZoneName: new("us-east-1"), + Encrypted: new(true), + FileSystemArn: new("arn:aws:elasticfilesystem:eu-west-2:944651592624:file-system/fs-0c6f2f41e957f42a9"), + KmsKeyId: new("arn:aws:kms:eu-west-2:944651592624:key/be76a6fa-d307-41c2-a4e3-cbfba2440747"), + Name: new("test"), + ProvisionedThroughputInMibps: new(float64(64)), ThroughputMode: types.ThroughputModeBursting, }, }, diff --git a/aws-source/adapters/efs-mount-target_test.go b/aws-source/adapters/efs-mount-target_test.go index 30885f3b..7732ed1b 100644 --- a/aws-source/adapters/efs-mount-target_test.go +++ b/aws-source/adapters/efs-mount-target_test.go @@ -14,16 +14,16 @@ func TestMountTargetOutputMapper(t *testing.T) { output := &efs.DescribeMountTargetsOutput{ MountTargets: []types.MountTargetDescription{ { - FileSystemId: PtrString("fs-1234567890"), + FileSystemId: new("fs-1234567890"), LifeCycleState: types.LifeCycleStateAvailable, - MountTargetId: PtrString("fsmt-01e86506d8165e43f"), - SubnetId: PtrString("subnet-1234567"), - AvailabilityZoneId: PtrString("use1-az1"), - AvailabilityZoneName: PtrString("us-east-1"), - IpAddress: PtrString("10.230.43.1"), - NetworkInterfaceId: PtrString("eni-2345"), - OwnerId: PtrString("234234"), - VpcId: PtrString("vpc-23452345235"), + MountTargetId: new("fsmt-01e86506d8165e43f"), + SubnetId: new("subnet-1234567"), + AvailabilityZoneId: new("use1-az1"), + AvailabilityZoneName: new("us-east-1"), + IpAddress: new("10.230.43.1"), + NetworkInterfaceId: new("eni-2345"), + OwnerId: new("234234"), + VpcId: new("vpc-23452345235"), }, }, } diff --git a/aws-source/adapters/efs-replication-configuration_test.go b/aws-source/adapters/efs-replication-configuration_test.go index d3cf233c..d9c37d49 100644 --- a/aws-source/adapters/efs-replication-configuration_test.go +++ b/aws-source/adapters/efs-replication-configuration_test.go @@ -13,25 +13,25 @@ func TestReplicationConfigurationOutputMapper(t *testing.T) { output := &efs.DescribeReplicationConfigurationsOutput{ Replications: []types.ReplicationConfigurationDescription{ { - CreationTime: PtrTime(time.Now()), + CreationTime: new(time.Now()), Destinations: []types.Destination{ { - FileSystemId: PtrString("fs-12345678"), - Region: PtrString("eu-west-1"), + FileSystemId: new("fs-12345678"), + Region: new("eu-west-1"), Status: types.ReplicationStatusEnabled, - LastReplicatedTimestamp: PtrTime(time.Now()), + LastReplicatedTimestamp: new(time.Now()), }, { - FileSystemId: PtrString("fs-98765432"), - Region: PtrString("us-west-2"), + FileSystemId: new("fs-98765432"), + Region: new("us-west-2"), Status: types.ReplicationStatusError, - LastReplicatedTimestamp: PtrTime(time.Now()), + LastReplicatedTimestamp: new(time.Now()), }, }, - OriginalSourceFileSystemArn: PtrString("arn:aws:elasticfilesystem:eu-west-2:944651592624:file-system/fs-0c6f2f41e957f42a9"), - SourceFileSystemArn: PtrString("arn:aws:elasticfilesystem:eu-west-2:944651592624:file-system/fs-0c6f2f41e957f42a9"), - SourceFileSystemId: PtrString("fs-748927493"), - SourceFileSystemRegion: PtrString("us-east-1"), + OriginalSourceFileSystemArn: new("arn:aws:elasticfilesystem:eu-west-2:944651592624:file-system/fs-0c6f2f41e957f42a9"), + SourceFileSystemArn: new("arn:aws:elasticfilesystem:eu-west-2:944651592624:file-system/fs-0c6f2f41e957f42a9"), + SourceFileSystemId: new("fs-748927493"), + SourceFileSystemRegion: new("us-east-1"), }, }, } diff --git a/aws-source/adapters/eks-addon_test.go b/aws-source/adapters/eks-addon_test.go index dacefe45..c0988fa1 100644 --- a/aws-source/adapters/eks-addon_test.go +++ b/aws-source/adapters/eks-addon_test.go @@ -13,24 +13,24 @@ import ( var AddonTestClient = EKSTestClient{ DescribeAddonOutput: &eks.DescribeAddonOutput{ Addon: &types.Addon{ - AddonName: PtrString("aws-ebs-csi-driver"), - ClusterName: PtrString("dylan"), + AddonName: new("aws-ebs-csi-driver"), + ClusterName: new("dylan"), Status: types.AddonStatusActive, - AddonVersion: PtrString("v1.13.0-eksbuild.3"), - ConfigurationValues: PtrString("values"), + AddonVersion: new("v1.13.0-eksbuild.3"), + ConfigurationValues: new("values"), MarketplaceInformation: &types.MarketplaceInformation{ - ProductId: PtrString("id"), - ProductUrl: PtrString("url"), + ProductId: new("id"), + ProductUrl: new("url"), }, - Publisher: PtrString("publisher"), - Owner: PtrString("owner"), + Publisher: new("publisher"), + Owner: new("owner"), Health: &types.AddonHealth{ Issues: []types.AddonIssue{}, }, - AddonArn: PtrString("arn:aws:eks:eu-west-2:801795385023:addon/dylan/aws-ebs-csi-driver/a2c29d0e-72c4-a702-7887-2f739f4fc189"), - CreatedAt: PtrTime(time.Now()), - ModifiedAt: PtrTime(time.Now()), - ServiceAccountRoleArn: PtrString("arn:aws:iam::801795385023:role/eks-csi-dylan"), + AddonArn: new("arn:aws:eks:eu-west-2:801795385023:addon/dylan/aws-ebs-csi-driver/a2c29d0e-72c4-a702-7887-2f739f4fc189"), + CreatedAt: new(time.Now()), + ModifiedAt: new(time.Now()), + ServiceAccountRoleArn: new("arn:aws:iam::801795385023:role/eks-csi-dylan"), }, }, } diff --git a/aws-source/adapters/eks-cluster_test.go b/aws-source/adapters/eks-cluster_test.go index 80dd9d3f..82232c1b 100644 --- a/aws-source/adapters/eks-cluster_test.go +++ b/aws-source/adapters/eks-cluster_test.go @@ -14,31 +14,31 @@ import ( var ClusterClient = EKSTestClient{ DescribeClusterOutput: &eks.DescribeClusterOutput{ Cluster: &types.Cluster{ - Name: PtrString("dylan"), - Arn: PtrString("arn:aws:eks:eu-west-2:801795385023:cluster/dylan"), - CreatedAt: PtrTime(time.Now()), - Version: PtrString("1.24"), - Endpoint: PtrString("https://00D3FF4CC48CBAA9BBC070DAA80BD251.gr7.eu-west-2.eks.amazonaws.com"), - RoleArn: PtrString("arn:aws:iam::801795385023:role/dylan-cluster-20221222134106992100000001"), - ClientRequestToken: PtrString("token"), + Name: new("dylan"), + Arn: new("arn:aws:eks:eu-west-2:801795385023:cluster/dylan"), + CreatedAt: new(time.Now()), + Version: new("1.24"), + Endpoint: new("https://00D3FF4CC48CBAA9BBC070DAA80BD251.gr7.eu-west-2.eks.amazonaws.com"), + RoleArn: new("arn:aws:iam::801795385023:role/dylan-cluster-20221222134106992100000001"), + ClientRequestToken: new("token"), ConnectorConfig: &types.ConnectorConfigResponse{ - ActivationCode: PtrString("code"), - ActivationExpiry: PtrTime(time.Now()), - ActivationId: PtrString("id"), - Provider: PtrString("provider"), - RoleArn: PtrString("arn:aws:iam::801795385023:role/dylan-cluster-20221222134106992100000002"), + ActivationCode: new("code"), + ActivationExpiry: new(time.Now()), + ActivationId: new("id"), + Provider: new("provider"), + RoleArn: new("arn:aws:iam::801795385023:role/dylan-cluster-20221222134106992100000002"), }, Health: &types.ClusterHealth{ Issues: []types.ClusterIssue{}, }, - Id: PtrString("id"), + Id: new("id"), OutpostConfig: &types.OutpostConfigResponse{ - ControlPlaneInstanceType: PtrString("type"), + ControlPlaneInstanceType: new("type"), OutpostArns: []string{ "arn1", }, ControlPlanePlacement: &types.ControlPlanePlacementResponse{ - GroupName: PtrString("groupName"), + GroupName: new("groupName"), }, }, ResourcesVpcConfig: &types.VpcConfigResponse{ @@ -50,8 +50,8 @@ var ClusterClient = EKSTestClient{ SecurityGroupIds: []string{ "sg-0bf38eb7e14777399", }, - ClusterSecurityGroupId: PtrString("sg-08df96f08566d4dda"), - VpcId: PtrString("vpc-0c9152ce7ed2b7305"), + ClusterSecurityGroupId: new("sg-08df96f08566d4dda"), + VpcId: new("vpc-0c9152ce7ed2b7305"), EndpointPublicAccess: true, EndpointPrivateAccess: true, PublicAccessCidrs: []string{ @@ -59,9 +59,9 @@ var ClusterClient = EKSTestClient{ }, }, KubernetesNetworkConfig: &types.KubernetesNetworkConfigResponse{ - ServiceIpv4Cidr: PtrString("172.20.0.0/16"), + ServiceIpv4Cidr: new("172.20.0.0/16"), IpFamily: types.IpFamilyIpv4, - ServiceIpv6Cidr: PtrString("ipv6cidr"), + ServiceIpv6Cidr: new("ipv6cidr"), }, Logging: &types.Logging{ ClusterLogging: []types.LogSetup{ @@ -72,26 +72,26 @@ var ClusterClient = EKSTestClient{ "controllerManager", "scheduler", }, - Enabled: PtrBool(true), + Enabled: new(true), }, { Types: []types.LogType{ "audit", }, - Enabled: PtrBool(false), + Enabled: new(false), }, }, }, Identity: &types.Identity{ Oidc: &types.OIDC{ - Issuer: PtrString("https://oidc.eks.eu-west-2.amazonaws.com/id/00D3FF4CC48CBAA9BBC070DAA80BD251"), + Issuer: new("https://oidc.eks.eu-west-2.amazonaws.com/id/00D3FF4CC48CBAA9BBC070DAA80BD251"), }, }, Status: types.ClusterStatusActive, CertificateAuthority: &types.Certificate{ - Data: PtrString("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJeU1USXlNakV6TkRZME5Gb1hEVE15TVRJeE9URXpORFkwTkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTC9tCkN6b25QdUZIUXM1a0xudzdCeXMrak9pNWJscEVCN2RhZUYvQzZqaEVTbkcwdVBVRjVWSFUzbmRyZHRKelBaemQKenM4U1pEMzRsKytGWmw0NFQrYWRqMGFYanpmZ0NTeFo4K0MvaWJUOWIzck5jWU9ZZ3FYT1lXc2JVYmpBSjRadgpnakFqdEl3dTBvUHNYT0JSZU5KTDlhRkl6VFFIcy9QL1hONWI5eGRlSHhwOXN4cnlEREYxQVNuQkxwajduUHMrCmgyNUtvd0hQV1luekV6WVd1T3NZbDQ2RjZacHh4aVhya2hnOGozckR4dXRWZGMvQVBFaVhUdHh3OU9CMjFDMkwKK1VpanpxS2RrZm5idVEvOHF0TTRqbFVGTkgzUG03STlkTEdIMTBTOFdhQkhpODNRMklCd3c0eE5RZ04xNC91dgpXWFZOWkxmM1EwbElkdmtxaCtrQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZCa2wvVEJwNVNyMFJrVEk2V1dMVkR4MVdZYUxNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQ0FCVWtZUWZSQXlRRFVsc2todgp2NTRZN3lFQ1lUSG00OWVtMWoyV2hyN0JPdXdlUkU4M3g1b0NhWEtjK2tMemlvOEVvY2hxOWN1a1FEYm1KNkpoCmRhUUlyaFFwaG5PMHZSd290YXlhWjdlV2IwTm50WmNxN1ZmNkp5ZU5CR3Y1NTJGdlNNcGprWnh0UXVpTTJ5TXoKbjJWWmtxMzJPb0RjTmxCMERhRVBCSjlIM2ZnbG1qcGdWL0NHZFdMNG1wNEpkb3VPNTFtNkJBMm1ET2JWYzh4VgppNFJIWE9KNG9hSGFTd1B6MHBuQUxabkJoUnpxV0Q1cGlycVlucjBxSlFDamJDWXF1TmJTU3d4c2JMYVFjanNFCjhiUXk0aGxXaEJNWno3UldOeDg1UTBZSjhWNEhKdXVCZ09MaVg1REFtNDZIbndWUy95MHJyN2JTWThoTXErM2QKTmtrPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="), + Data: new("LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMvakNDQWVhZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJeU1USXlNakV6TkRZME5Gb1hEVE15TVRJeE9URXpORFkwTkZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTC9tCkN6b25QdUZIUXM1a0xudzdCeXMrak9pNWJscEVCN2RhZUYvQzZqaEVTbkcwdVBVRjVWSFUzbmRyZHRKelBaemQKenM4U1pEMzRsKytGWmw0NFQrYWRqMGFYanpmZ0NTeFo4K0MvaWJUOWIzck5jWU9ZZ3FYT1lXc2JVYmpBSjRadgpnakFqdEl3dTBvUHNYT0JSZU5KTDlhRkl6VFFIcy9QL1hONWI5eGRlSHhwOXN4cnlEREYxQVNuQkxwajduUHMrCmgyNUtvd0hQV1luekV6WVd1T3NZbDQ2RjZacHh4aVhya2hnOGozckR4dXRWZGMvQVBFaVhUdHh3OU9CMjFDMkwKK1VpanpxS2RrZm5idVEvOHF0TTRqbFVGTkgzUG03STlkTEdIMTBTOFdhQkhpODNRMklCd3c0eE5RZ04xNC91dgpXWFZOWkxmM1EwbElkdmtxaCtrQ0F3RUFBYU5aTUZjd0RnWURWUjBQQVFIL0JBUURBZ0trTUE4R0ExVWRFd0VCCi93UUZNQU1CQWY4d0hRWURWUjBPQkJZRUZCa2wvVEJwNVNyMFJrVEk2V1dMVkR4MVdZYUxNQlVHQTFVZEVRUU8KTUF5Q0NtdDFZbVZ5Ym1WMFpYTXdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUJBQ0FCVWtZUWZSQXlRRFVsc2todgp2NTRZN3lFQ1lUSG00OWVtMWoyV2hyN0JPdXdlUkU4M3g1b0NhWEtjK2tMemlvOEVvY2hxOWN1a1FEYm1KNkpoCmRhUUlyaFFwaG5PMHZSd290YXlhWjdlV2IwTm50WmNxN1ZmNkp5ZU5CR3Y1NTJGdlNNcGprWnh0UXVpTTJ5TXoKbjJWWmtxMzJPb0RjTmxCMERhRVBCSjlIM2ZnbG1qcGdWL0NHZFdMNG1wNEpkb3VPNTFtNkJBMm1ET2JWYzh4VgppNFJIWE9KNG9hSGFTd1B6MHBuQUxabkJoUnpxV0Q1cGlycVlucjBxSlFDamJDWXF1TmJTU3d4c2JMYVFjanNFCjhiUXk0aGxXaEJNWno3UldOeDg1UTBZSjhWNEhKdXVCZ09MaVg1REFtNDZIbndWUy95MHJyN2JTWThoTXErM2QKTmtrPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg=="), }, - PlatformVersion: PtrString("eks.3"), + PlatformVersion: new("eks.3"), Tags: map[string]string{}, EncryptionConfig: []types.EncryptionConfig{ { @@ -99,7 +99,7 @@ var ClusterClient = EKSTestClient{ "secrets", }, Provider: &types.Provider{ - KeyArn: PtrString("arn:aws:kms:eu-west-2:801795385023:key/3a478539-9717-4c20-83a5-19989154dc32"), + KeyArn: new("arn:aws:kms:eu-west-2:801795385023:key/3a478539-9717-4c20-83a5-19989154dc32"), }, }, }, diff --git a/aws-source/adapters/eks-fargate-profile_test.go b/aws-source/adapters/eks-fargate-profile_test.go index 6d6cf7db..65215362 100644 --- a/aws-source/adapters/eks-fargate-profile_test.go +++ b/aws-source/adapters/eks-fargate-profile_test.go @@ -14,15 +14,15 @@ import ( var FargateTestClient = EKSTestClient{ DescribeFargateProfileOutput: &eks.DescribeFargateProfileOutput{ FargateProfile: &types.FargateProfile{ - ClusterName: PtrString("cluster"), - CreatedAt: PtrTime(time.Now()), - FargateProfileArn: PtrString("arn:partition:service:region:account-id:resource-type/resource-id"), - FargateProfileName: PtrString("name"), - PodExecutionRoleArn: PtrString("arn:partition:service::account-id:resource-type/resource-id"), + ClusterName: new("cluster"), + CreatedAt: new(time.Now()), + FargateProfileArn: new("arn:partition:service:region:account-id:resource-type/resource-id"), + FargateProfileName: new("name"), + PodExecutionRoleArn: new("arn:partition:service::account-id:resource-type/resource-id"), Selectors: []types.FargateProfileSelector{ { Labels: map[string]string{}, - Namespace: PtrString("namespace"), + Namespace: new("namespace"), }, }, Status: types.FargateProfileStatusActive, diff --git a/aws-source/adapters/eks-nodegroup_test.go b/aws-source/adapters/eks-nodegroup_test.go index c630f329..f2d9424b 100644 --- a/aws-source/adapters/eks-nodegroup_test.go +++ b/aws-source/adapters/eks-nodegroup_test.go @@ -14,26 +14,26 @@ import ( var NodeGroupClient = EKSTestClient{ DescribeNodegroupOutput: &eks.DescribeNodegroupOutput{ Nodegroup: &types.Nodegroup{ - NodegroupName: PtrString("default-2022122213523169820000001f"), - NodegroupArn: PtrString("arn:aws:eks:eu-west-2:801795385023:nodegroup/dylan/default-2022122213523169820000001f/98c29d0d-b22a-aaa3-445e-ebf71d43f67c"), - ClusterName: PtrString("dylan"), - Version: PtrString("1.24"), - ReleaseVersion: PtrString("1.24.7-20221112"), - CreatedAt: PtrTime(time.Now()), - ModifiedAt: PtrTime(time.Now()), + NodegroupName: new("default-2022122213523169820000001f"), + NodegroupArn: new("arn:aws:eks:eu-west-2:801795385023:nodegroup/dylan/default-2022122213523169820000001f/98c29d0d-b22a-aaa3-445e-ebf71d43f67c"), + ClusterName: new("dylan"), + Version: new("1.24"), + ReleaseVersion: new("1.24.7-20221112"), + CreatedAt: new(time.Now()), + ModifiedAt: new(time.Now()), Status: types.NodegroupStatusActive, CapacityType: types.CapacityTypesOnDemand, - DiskSize: PtrInt32(100), + DiskSize: new(int32(100)), RemoteAccess: &types.RemoteAccessConfig{ - Ec2SshKey: PtrString("key"), // link + Ec2SshKey: new("key"), // link SourceSecurityGroups: []string{ "sg1", // link }, }, ScalingConfig: &types.NodegroupScalingConfig{ - MinSize: PtrInt32(1), - MaxSize: PtrInt32(3), - DesiredSize: PtrInt32(1), + MinSize: new(int32(1)), + MaxSize: new(int32(3)), + DesiredSize: new(int32(1)), }, InstanceTypes: []string{ "T3large", @@ -42,33 +42,33 @@ var NodeGroupClient = EKSTestClient{ "subnet0d1fabfe6794b5543", // link }, AmiType: types.AMITypesAl2Arm64, - NodeRole: PtrString("arn:aws:iam::801795385023:role/default-eks-node-group-20221222134106992000000003"), + NodeRole: new("arn:aws:iam::801795385023:role/default-eks-node-group-20221222134106992000000003"), Labels: map[string]string{}, Taints: []types.Taint{ { Effect: types.TaintEffectNoSchedule, - Key: PtrString("key"), - Value: PtrString("value"), + Key: new("key"), + Value: new("value"), }, }, Resources: &types.NodegroupResources{ AutoScalingGroups: []types.AutoScalingGroup{ { - Name: PtrString("eks-default-2022122213523169820000001f-98c29d0d-b22a-aaa3-445e-ebf71d43f67c"), // link + Name: new("eks-default-2022122213523169820000001f-98c29d0d-b22a-aaa3-445e-ebf71d43f67c"), // link }, }, - RemoteAccessSecurityGroup: PtrString("sg2"), // link + RemoteAccessSecurityGroup: new("sg2"), // link }, Health: &types.NodegroupHealth{ Issues: []types.Issue{}, }, UpdateConfig: &types.NodegroupUpdateConfig{ - MaxUnavailablePercentage: PtrInt32(33), + MaxUnavailablePercentage: new(int32(33)), }, LaunchTemplate: &types.LaunchTemplateSpecification{ - Name: PtrString("default-2022122213523100410000001d"), // link - Version: PtrString("1"), - Id: PtrString("lt-097e994ce7e14fcdc"), + Name: new("default-2022122213523100410000001d"), // link + Version: new("1"), + Id: new("lt-097e994ce7e14fcdc"), }, Tags: map[string]string{}, }, diff --git a/aws-source/adapters/elb-instance-health_test.go b/aws-source/adapters/elb-instance-health_test.go index de83a9cc..e4375456 100644 --- a/aws-source/adapters/elb-instance-health_test.go +++ b/aws-source/adapters/elb-instance-health_test.go @@ -15,10 +15,10 @@ func TestInstanceHealthOutputMapper(t *testing.T) { output := elb.DescribeInstanceHealthOutput{ InstanceStates: []types.InstanceState{ { - InstanceId: PtrString("i-0337802d908b4a81e"), // link - State: PtrString("InService"), - ReasonCode: PtrString("N/A"), - Description: PtrString("N/A"), + InstanceId: new("i-0337802d908b4a81e"), // link + State: new("InService"), + ReasonCode: new("N/A"), + Description: new("N/A"), }, }, } diff --git a/aws-source/adapters/elb-load-balancer_test.go b/aws-source/adapters/elb-load-balancer_test.go index e54a56c9..aea8fb63 100644 --- a/aws-source/adapters/elb-load-balancer_test.go +++ b/aws-source/adapters/elb-load-balancer_test.go @@ -17,11 +17,11 @@ func (m mockElbClient) DescribeTags(ctx context.Context, params *elb.DescribeTag return &elb.DescribeTagsOutput{ TagDescriptions: []types.TagDescription{ { - LoadBalancerName: PtrString("a8c3c8851f0df43fda89797c8e941a91"), + LoadBalancerName: new("a8c3c8851f0df43fda89797c8e941a91"), Tags: []types.Tag{ { - Key: PtrString("foo"), - Value: PtrString("bar"), + Key: new("foo"), + Value: new("bar"), }, }, }, @@ -37,35 +37,35 @@ func TestELBv2LoadBalancerOutputMapper(t *testing.T) { output := &elb.DescribeLoadBalancersOutput{ LoadBalancerDescriptions: []types.LoadBalancerDescription{ { - LoadBalancerName: PtrString("a8c3c8851f0df43fda89797c8e941a91"), - DNSName: PtrString("a8c3c8851f0df43fda89797c8e941a91-182843316.eu-west-2.elb.amazonaws.com"), // link - CanonicalHostedZoneName: PtrString("a8c3c8851f0df43fda89797c8e941a91-182843316.eu-west-2.elb.amazonaws.com"), // link - CanonicalHostedZoneNameID: PtrString("ZHURV8PSTC4K8"), // link + LoadBalancerName: new("a8c3c8851f0df43fda89797c8e941a91"), + DNSName: new("a8c3c8851f0df43fda89797c8e941a91-182843316.eu-west-2.elb.amazonaws.com"), // link + CanonicalHostedZoneName: new("a8c3c8851f0df43fda89797c8e941a91-182843316.eu-west-2.elb.amazonaws.com"), // link + CanonicalHostedZoneNameID: new("ZHURV8PSTC4K8"), // link ListenerDescriptions: []types.ListenerDescription{ { Listener: &types.Listener{ - Protocol: PtrString("TCP"), + Protocol: new("TCP"), LoadBalancerPort: 7687, - InstanceProtocol: PtrString("TCP"), - InstancePort: PtrInt32(30133), + InstanceProtocol: new("TCP"), + InstancePort: new(int32(30133)), }, PolicyNames: []string{}, }, { Listener: &types.Listener{ - Protocol: PtrString("TCP"), + Protocol: new("TCP"), LoadBalancerPort: 7473, - InstanceProtocol: PtrString("TCP"), - InstancePort: PtrInt32(31459), + InstanceProtocol: new("TCP"), + InstancePort: new(int32(31459)), }, PolicyNames: []string{}, }, { Listener: &types.Listener{ - Protocol: PtrString("TCP"), + Protocol: new("TCP"), LoadBalancerPort: 7474, - InstanceProtocol: PtrString("TCP"), - InstancePort: PtrInt32(30761), + InstanceProtocol: new("TCP"), + InstancePort: new(int32(30761)), }, PolicyNames: []string{}, }, @@ -73,21 +73,21 @@ func TestELBv2LoadBalancerOutputMapper(t *testing.T) { Policies: &types.Policies{ AppCookieStickinessPolicies: []types.AppCookieStickinessPolicy{ { - CookieName: PtrString("foo"), - PolicyName: PtrString("policy"), + CookieName: new("foo"), + PolicyName: new("policy"), }, }, LBCookieStickinessPolicies: []types.LBCookieStickinessPolicy{ { - CookieExpirationPeriod: PtrInt64(10), - PolicyName: PtrString("name"), + CookieExpirationPeriod: new(int64(10)), + PolicyName: new("name"), }, }, OtherPolicies: []string{}, }, BackendServerDescriptions: []types.BackendServerDescription{ { - InstancePort: PtrInt32(443), + InstancePort: new(int32(443)), PolicyNames: []string{}, }, }, @@ -101,28 +101,28 @@ func TestELBv2LoadBalancerOutputMapper(t *testing.T) { "subnet09d5f6fa75b0b4569", "subnet0e234bef35fc4a9e1", }, - VPCId: PtrString("vpc-0c72199250cd479ea"), // link + VPCId: new("vpc-0c72199250cd479ea"), // link Instances: []types.Instance{ { - InstanceId: PtrString("i-0337802d908b4a81e"), // link *2 to ec2-instance and health + InstanceId: new("i-0337802d908b4a81e"), // link *2 to ec2-instance and health }, }, HealthCheck: &types.HealthCheck{ - Target: PtrString("HTTP:31151/healthz"), - Interval: PtrInt32(10), - Timeout: PtrInt32(5), - UnhealthyThreshold: PtrInt32(6), - HealthyThreshold: PtrInt32(2), + Target: new("HTTP:31151/healthz"), + Interval: new(int32(10)), + Timeout: new(int32(5)), + UnhealthyThreshold: new(int32(6)), + HealthyThreshold: new(int32(2)), }, SourceSecurityGroup: &types.SourceSecurityGroup{ - OwnerAlias: PtrString("944651592624"), - GroupName: PtrString("k8s-elb-a8c3c8851f0df43fda89797c8e941a91"), // link + OwnerAlias: new("944651592624"), + GroupName: new("k8s-elb-a8c3c8851f0df43fda89797c8e941a91"), // link }, SecurityGroups: []string{ "sg097e3cfdfc6d53b77", // link }, - CreatedTime: PtrTime(time.Now()), - Scheme: PtrString("internet-facing"), + CreatedTime: new(time.Now()), + Scheme: new("internet-facing"), }, }, } diff --git a/aws-source/adapters/elbv2-listener.go b/aws-source/adapters/elbv2-listener.go index a6950275..59bfbcbd 100644 --- a/aws-source/adapters/elbv2-listener.go +++ b/aws-source/adapters/elbv2-listener.go @@ -37,9 +37,9 @@ func listenerOutputMapper(ctx context.Context, client elbv2Client, scope string, sha := base64.URLEncoding.EncodeToString(h.Sum(nil)) if len(sha) > 12 { - action.AuthenticateOidcConfig.ClientSecret = PtrString(fmt.Sprintf("REDACTED (Version: %v)", sha[:11])) + action.AuthenticateOidcConfig.ClientSecret = new(fmt.Sprintf("REDACTED (Version: %v)", sha[:11])) } else { - action.AuthenticateOidcConfig.ClientSecret = PtrString("[REDACTED]") + action.AuthenticateOidcConfig.ClientSecret = new("[REDACTED]") } } } diff --git a/aws-source/adapters/elbv2-listener_test.go b/aws-source/adapters/elbv2-listener_test.go index 01a3eb6c..f7b0ec6a 100644 --- a/aws-source/adapters/elbv2-listener_test.go +++ b/aws-source/adapters/elbv2-listener_test.go @@ -14,17 +14,17 @@ func TestListenerOutputMapper(t *testing.T) { output := elbv2.DescribeListenersOutput{ Listeners: []types.Listener{ { - ListenerArn: PtrString("arn:aws:elasticloadbalancing:eu-west-2:944651592624:listener/app/ingress/1bf10920c5bd199d/9d28f512be129134"), - LoadBalancerArn: PtrString("arn:aws:elasticloadbalancing:eu-west-2:944651592624:loadbalancer/app/ingress/1bf10920c5bd199d"), // link - Port: PtrInt32(443), + ListenerArn: new("arn:aws:elasticloadbalancing:eu-west-2:944651592624:listener/app/ingress/1bf10920c5bd199d/9d28f512be129134"), + LoadBalancerArn: new("arn:aws:elasticloadbalancing:eu-west-2:944651592624:loadbalancer/app/ingress/1bf10920c5bd199d"), // link + Port: new(int32(443)), Protocol: types.ProtocolEnumHttps, Certificates: []types.Certificate{ { - CertificateArn: PtrString("arn:aws:acm:eu-west-2:944651592624:certificate/acd84d34-fb78-4411-bd8a-43684a3477c5"), // link - IsDefault: PtrBool(true), + CertificateArn: new("arn:aws:acm:eu-west-2:944651592624:certificate/acd84d34-fb78-4411-bd8a-43684a3477c5"), // link + IsDefault: new(true), }, }, - SslPolicy: PtrString("ELBSecurityPolicy-2016-08"), + SslPolicy: new("ELBSecurityPolicy-2016-08"), AlpnPolicy: []string{ "policy1", }, diff --git a/aws-source/adapters/elbv2-load-balancer_test.go b/aws-source/adapters/elbv2-load-balancer_test.go index ed37a0a7..765889e7 100644 --- a/aws-source/adapters/elbv2-load-balancer_test.go +++ b/aws-source/adapters/elbv2-load-balancer_test.go @@ -15,38 +15,38 @@ func TestLoadBalancerOutputMapper(t *testing.T) { output := elbv2.DescribeLoadBalancersOutput{ LoadBalancers: []types.LoadBalancer{ { - LoadBalancerArn: PtrString("arn:aws:elasticloadbalancing:eu-west-2:944651592624:loadbalancer/app/ingress/1bf10920c5bd199d"), - DNSName: PtrString("ingress-1285969159.eu-west-2.elb.amazonaws.com"), // link - CanonicalHostedZoneId: PtrString("ZHURV8PSTC4K8"), // link - CreatedTime: PtrTime(time.Now()), - LoadBalancerName: PtrString("ingress"), + LoadBalancerArn: new("arn:aws:elasticloadbalancing:eu-west-2:944651592624:loadbalancer/app/ingress/1bf10920c5bd199d"), + DNSName: new("ingress-1285969159.eu-west-2.elb.amazonaws.com"), // link + CanonicalHostedZoneId: new("ZHURV8PSTC4K8"), // link + CreatedTime: new(time.Now()), + LoadBalancerName: new("ingress"), Scheme: types.LoadBalancerSchemeEnumInternetFacing, - VpcId: PtrString("vpc-0c72199250cd479ea"), // link + VpcId: new("vpc-0c72199250cd479ea"), // link State: &types.LoadBalancerState{ Code: types.LoadBalancerStateEnumActive, - Reason: PtrString("reason"), + Reason: new("reason"), }, Type: types.LoadBalancerTypeEnumApplication, AvailabilityZones: []types.AvailabilityZone{ { - ZoneName: PtrString("eu-west-2b"), // link - SubnetId: PtrString("subnet-0960234bbc4edca03"), // link + ZoneName: new("eu-west-2b"), // link + SubnetId: new("subnet-0960234bbc4edca03"), // link LoadBalancerAddresses: []types.LoadBalancerAddress{ { - AllocationId: PtrString("allocation-id"), // link? - IPv6Address: PtrString(":::1"), // link - IpAddress: PtrString("1.1.1.1"), // link - PrivateIPv4Address: PtrString("10.0.0.1"), // link + AllocationId: new("allocation-id"), // link? + IPv6Address: new(":::1"), // link + IpAddress: new("1.1.1.1"), // link + PrivateIPv4Address: new("10.0.0.1"), // link }, }, - OutpostId: PtrString("outpost-id"), + OutpostId: new("outpost-id"), }, }, SecurityGroups: []string{ "sg-0b21edc8578ea3f93", // link }, IpAddressType: types.IpAddressTypeIpv4, - CustomerOwnedIpv4Pool: PtrString("ipv4-pool"), // link + CustomerOwnedIpv4Pool: new("ipv4-pool"), // link }, }, } diff --git a/aws-source/adapters/elbv2-rule_test.go b/aws-source/adapters/elbv2-rule_test.go index 1cdd7024..7cc3d3bb 100644 --- a/aws-source/adapters/elbv2-rule_test.go +++ b/aws-source/adapters/elbv2-rule_test.go @@ -18,11 +18,11 @@ func TestRuleOutputMapper(t *testing.T) { output := elbv2.DescribeRulesOutput{ Rules: []types.Rule{ { - RuleArn: PtrString("arn:aws:elasticloadbalancing:eu-west-2:944651592624:listener-rule/app/ingress/1bf10920c5bd199d/9d28f512be129134/0f73a74d21b008f7"), - Priority: PtrString("1"), + RuleArn: new("arn:aws:elasticloadbalancing:eu-west-2:944651592624:listener-rule/app/ingress/1bf10920c5bd199d/9d28f512be129134/0f73a74d21b008f7"), + Priority: new("1"), Conditions: []types.RuleCondition{ { - Field: PtrString("path-pattern"), + Field: new("path-pattern"), Values: []string{ "/api/gateway", }, @@ -37,7 +37,7 @@ func TestRuleOutputMapper(t *testing.T) { }, }, HttpHeaderConfig: &types.HttpHeaderConditionConfig{ - HttpHeaderName: PtrString("SOMETHING"), + HttpHeaderName: new("SOMETHING"), Values: []string{ "foo", }, @@ -50,8 +50,8 @@ func TestRuleOutputMapper(t *testing.T) { QueryStringConfig: &types.QueryStringConditionConfig{ Values: []types.QueryStringKeyValuePair{ { - Key: PtrString("foo"), - Value: PtrString("bar"), + Key: new("foo"), + Value: new("bar"), }, }, }, @@ -65,7 +65,7 @@ func TestRuleOutputMapper(t *testing.T) { Actions: []types.Action{ // Tested in actions.go }, - IsDefault: PtrBool(false), + IsDefault: new(false), }, }, } diff --git a/aws-source/adapters/elbv2-target-group_test.go b/aws-source/adapters/elbv2-target-group_test.go index 44ec62ba..816ddddf 100644 --- a/aws-source/adapters/elbv2-target-group_test.go +++ b/aws-source/adapters/elbv2-target-group_test.go @@ -16,28 +16,28 @@ func TestTargetGroupOutputMapper(t *testing.T) { output := elbv2.DescribeTargetGroupsOutput{ TargetGroups: []types.TargetGroup{ { - TargetGroupArn: PtrString("arn:aws:elasticloadbalancing:eu-west-2:944651592624:targetgroup/k8s-default-apiserve-d87e8f7010/559d207158e41222"), - TargetGroupName: PtrString("k8s-default-apiserve-d87e8f7010"), + TargetGroupArn: new("arn:aws:elasticloadbalancing:eu-west-2:944651592624:targetgroup/k8s-default-apiserve-d87e8f7010/559d207158e41222"), + TargetGroupName: new("k8s-default-apiserve-d87e8f7010"), Protocol: types.ProtocolEnumHttp, - Port: PtrInt32(8080), - VpcId: PtrString("vpc-0c72199250cd479ea"), // link + Port: new(int32(8080)), + VpcId: new("vpc-0c72199250cd479ea"), // link HealthCheckProtocol: types.ProtocolEnumHttp, - HealthCheckPort: PtrString("traffic-port"), - HealthCheckEnabled: PtrBool(true), - HealthCheckIntervalSeconds: PtrInt32(10), - HealthCheckTimeoutSeconds: PtrInt32(10), - HealthyThresholdCount: PtrInt32(10), - UnhealthyThresholdCount: PtrInt32(10), - HealthCheckPath: PtrString("/"), + HealthCheckPort: new("traffic-port"), + HealthCheckEnabled: new(true), + HealthCheckIntervalSeconds: new(int32(10)), + HealthCheckTimeoutSeconds: new(int32(10)), + HealthyThresholdCount: new(int32(10)), + UnhealthyThresholdCount: new(int32(10)), + HealthCheckPath: new("/"), Matcher: &types.Matcher{ - HttpCode: PtrString("200"), - GrpcCode: PtrString("code"), + HttpCode: new("200"), + GrpcCode: new("code"), }, LoadBalancerArns: []string{ "arn:aws:elasticloadbalancing:eu-west-2:944651592624:loadbalancer/app/ingress/1bf10920c5bd199d", // link }, TargetType: types.TargetTypeEnumIp, - ProtocolVersion: PtrString("HTTP1"), + ProtocolVersion: new("HTTP1"), IpAddressType: types.TargetGroupIpAddressTypeEnumIpv4, }, }, diff --git a/aws-source/adapters/elbv2-target-health_test.go b/aws-source/adapters/elbv2-target-health_test.go index c15e8a62..2d884172 100644 --- a/aws-source/adapters/elbv2-target-health_test.go +++ b/aws-source/adapters/elbv2-target-health_test.go @@ -15,61 +15,61 @@ func TestTargetHealthOutputMapper(t *testing.T) { TargetHealthDescriptions: []types.TargetHealthDescription{ { Target: &types.TargetDescription{ - Id: PtrString("10.0.6.64"), // link - Port: PtrInt32(8080), - AvailabilityZone: PtrString("eu-west-2c"), + Id: new("10.0.6.64"), // link + Port: new(int32(8080)), + AvailabilityZone: new("eu-west-2c"), }, - HealthCheckPort: PtrString("8080"), + HealthCheckPort: new("8080"), TargetHealth: &types.TargetHealth{ State: types.TargetHealthStateEnumHealthy, Reason: types.TargetHealthReasonEnumDeregistrationInProgress, - Description: PtrString("Health checks failed with these codes: [404]"), + Description: new("Health checks failed with these codes: [404]"), }, }, { Target: &types.TargetDescription{ - Id: PtrString("arn:aws:elasticloadbalancing:eu-west-2:944651592624:loadbalancer/app/ingress/1bf10920c5bd199d"), // link - Port: PtrInt32(8080), - AvailabilityZone: PtrString("eu-west-2c"), + Id: new("arn:aws:elasticloadbalancing:eu-west-2:944651592624:loadbalancer/app/ingress/1bf10920c5bd199d"), // link + Port: new(int32(8080)), + AvailabilityZone: new("eu-west-2c"), }, - HealthCheckPort: PtrString("8080"), + HealthCheckPort: new("8080"), TargetHealth: &types.TargetHealth{ State: types.TargetHealthStateEnumHealthy, Reason: types.TargetHealthReasonEnumDeregistrationInProgress, - Description: PtrString("Health checks failed with these codes: [404]"), + Description: new("Health checks failed with these codes: [404]"), }, }, { Target: &types.TargetDescription{ - Id: PtrString("i-foo"), // link - Port: PtrInt32(8080), - AvailabilityZone: PtrString("eu-west-2c"), + Id: new("i-foo"), // link + Port: new(int32(8080)), + AvailabilityZone: new("eu-west-2c"), }, - HealthCheckPort: PtrString("8080"), + HealthCheckPort: new("8080"), TargetHealth: &types.TargetHealth{ State: types.TargetHealthStateEnumHealthy, Reason: types.TargetHealthReasonEnumDeregistrationInProgress, - Description: PtrString("Health checks failed with these codes: [404]"), + Description: new("Health checks failed with these codes: [404]"), }, }, { Target: &types.TargetDescription{ - Id: PtrString("arn:aws:lambda:eu-west-2:944651592624:function/foobar"), // link - Port: PtrInt32(8080), - AvailabilityZone: PtrString("eu-west-2c"), + Id: new("arn:aws:lambda:eu-west-2:944651592624:function/foobar"), // link + Port: new(int32(8080)), + AvailabilityZone: new("eu-west-2c"), }, - HealthCheckPort: PtrString("8080"), + HealthCheckPort: new("8080"), TargetHealth: &types.TargetHealth{ State: types.TargetHealthStateEnumHealthy, Reason: types.TargetHealthReasonEnumDeregistrationInProgress, - Description: PtrString("Health checks failed with these codes: [404]"), + Description: new("Health checks failed with these codes: [404]"), }, }, }, } items, err := targetHealthOutputMapper(context.Background(), nil, "foo", &elbv2.DescribeTargetHealthInput{ - TargetGroupArn: PtrString("arn:aws:elasticloadbalancing:eu-west-2:944651592624:targetgroup/k8s-default-apiserve-d87e8f7010/559d207158e41222"), + TargetGroupArn: new("arn:aws:elasticloadbalancing:eu-west-2:944651592624:targetgroup/k8s-default-apiserve-d87e8f7010/559d207158e41222"), }, &output) if err != nil { @@ -167,8 +167,8 @@ func TestTargetHealthUniqueID(t *testing.T) { id := TargetHealthUniqueID{ TargetGroupArn: "arn:aws:elasticloadbalancing:eu-west-2:944651592624:targetgroup/k8s-default-apiserve-d87e8f7010/559d207158e41222", Id: "10.0.0.1", - AvailabilityZone: PtrString("eu-west-2"), - Port: PtrInt32(8080), + AvailabilityZone: new("eu-west-2"), + Port: new(int32(8080)), } expected := "arn:aws:elasticloadbalancing:eu-west-2:944651592624:targetgroup/k8s-default-apiserve-d87e8f7010/559d207158e41222|10.0.0.1|eu-west-2|8080" @@ -192,7 +192,7 @@ func TestTargetHealthUniqueID(t *testing.T) { id := TargetHealthUniqueID{ TargetGroupArn: "arn:aws:elasticloadbalancing:eu-west-2:944651592624:targetgroup/k8s-default-apiserve-d87e8f7010/559d207158e41222", Id: "arn:partition:service:region:account-id:resource-type:resource-id", - Port: PtrInt32(8080), + Port: new(int32(8080)), } expected := "arn:aws:elasticloadbalancing:eu-west-2:944651592624:targetgroup/k8s-default-apiserve-d87e8f7010/559d207158e41222|arn:partition:service:region:account-id:resource-type:resource-id||8080" diff --git a/aws-source/adapters/elbv2_test.go b/aws-source/adapters/elbv2_test.go index 6079768a..1a1f991e 100644 --- a/aws-source/adapters/elbv2_test.go +++ b/aws-source/adapters/elbv2_test.go @@ -19,8 +19,8 @@ func (m mockElbv2Client) DescribeTags(ctx context.Context, params *elbv2.Describ ResourceArn: &arn, Tags: []types.Tag{ { - Key: PtrString("foo"), - Value: PtrString("bar"), + Key: new("foo"), + Value: new("bar"), }, }, }) @@ -50,59 +50,59 @@ func (m mockElbv2Client) DescribeTargetGroups(ctx context.Context, params *elbv2 func TestActionToRequests(t *testing.T) { action := types.Action{ Type: types.ActionTypeEnumFixedResponse, - Order: PtrInt32(1), + Order: new(int32(1)), FixedResponseConfig: &types.FixedResponseActionConfig{ - StatusCode: PtrString("404"), - ContentType: PtrString("text/plain"), - MessageBody: PtrString("not found"), + StatusCode: new("404"), + ContentType: new("text/plain"), + MessageBody: new("not found"), }, AuthenticateCognitoConfig: &types.AuthenticateCognitoActionConfig{ - UserPoolArn: PtrString("arn:partition:service:region:account-id:resource-type:resource-id"), // link - UserPoolClientId: PtrString("clientID"), - UserPoolDomain: PtrString("domain.com"), + UserPoolArn: new("arn:partition:service:region:account-id:resource-type:resource-id"), // link + UserPoolClientId: new("clientID"), + UserPoolDomain: new("domain.com"), AuthenticationRequestExtraParams: map[string]string{ "foo": "bar", }, OnUnauthenticatedRequest: types.AuthenticateCognitoActionConditionalBehaviorEnumAuthenticate, - Scope: PtrString("foo"), - SessionCookieName: PtrString("cookie"), - SessionTimeout: PtrInt64(10), + Scope: new("foo"), + SessionCookieName: new("cookie"), + SessionTimeout: new(int64(10)), }, AuthenticateOidcConfig: &types.AuthenticateOidcActionConfig{ - AuthorizationEndpoint: PtrString("https://auth.somewhere.com/app1"), // link - ClientId: PtrString("CLIENT-ID"), - Issuer: PtrString("Someone"), - TokenEndpoint: PtrString("https://auth.somewhere.com/app1/tokens"), // link - UserInfoEndpoint: PtrString("https://auth.somewhere.com/app1/users"), // link + AuthorizationEndpoint: new("https://auth.somewhere.com/app1"), // link + ClientId: new("CLIENT-ID"), + Issuer: new("Someone"), + TokenEndpoint: new("https://auth.somewhere.com/app1/tokens"), // link + UserInfoEndpoint: new("https://auth.somewhere.com/app1/users"), // link AuthenticationRequestExtraParams: map[string]string{}, - ClientSecret: PtrString("secret"), // Redact + ClientSecret: new("secret"), // Redact OnUnauthenticatedRequest: types.AuthenticateOidcActionConditionalBehaviorEnumAllow, - Scope: PtrString("foo"), - SessionCookieName: PtrString("cookie"), - SessionTimeout: PtrInt64(10), - UseExistingClientSecret: PtrBool(true), + Scope: new("foo"), + SessionCookieName: new("cookie"), + SessionTimeout: new(int64(10)), + UseExistingClientSecret: new(true), }, ForwardConfig: &types.ForwardActionConfig{ TargetGroupStickinessConfig: &types.TargetGroupStickinessConfig{ - DurationSeconds: PtrInt32(10), - Enabled: PtrBool(true), + DurationSeconds: new(int32(10)), + Enabled: new(true), }, TargetGroups: []types.TargetGroupTuple{ { - TargetGroupArn: PtrString("arn:partition:service:region:account-id:resource-type:resource-id1"), // link - Weight: PtrInt32(1), + TargetGroupArn: new("arn:partition:service:region:account-id:resource-type:resource-id1"), // link + Weight: new(int32(1)), }, }, }, RedirectConfig: &types.RedirectActionConfig{ StatusCode: types.RedirectActionStatusCodeEnumHttp302, - Host: PtrString("somewhere.else.com"), // combine and link - Path: PtrString("/login"), // combine and link - Port: PtrString("8080"), // combine and link - Protocol: PtrString("https"), // combine and link - Query: PtrString("foo=bar"), // combine and link + Host: new("somewhere.else.com"), // combine and link + Path: new("/login"), // combine and link + Port: new("8080"), // combine and link + Protocol: new("https"), // combine and link + Query: new("foo=bar"), // combine and link }, - TargetGroupArn: PtrString("arn:partition:service:region:account-id:resource-type:resource-id2"), // link + TargetGroupArn: new("arn:partition:service:region:account-id:resource-type:resource-id2"), // link } item := sdp.Item{ diff --git a/aws-source/adapters/iam-group_test.go b/aws-source/adapters/iam-group_test.go index 2ad67320..5778aa10 100644 --- a/aws-source/adapters/iam-group_test.go +++ b/aws-source/adapters/iam-group_test.go @@ -12,11 +12,11 @@ import ( func TestGroupItemMapper(t *testing.T) { zone := types.Group{ - Path: PtrString("/"), - GroupName: PtrString("power-users"), - GroupId: PtrString("AGPA3VLV2U27T6SSLJMDS"), - Arn: PtrString("arn:aws:iam::801795385023:group/power-users"), - CreateDate: PtrTime(time.Now()), + Path: new("/"), + GroupName: new("power-users"), + GroupId: new("AGPA3VLV2U27T6SSLJMDS"), + Arn: new("arn:aws:iam::801795385023:group/power-users"), + CreateDate: new(time.Now()), } item, err := groupItemMapper(nil, "foo", &zone) diff --git a/aws-source/adapters/iam-instance-profile_test.go b/aws-source/adapters/iam-instance-profile_test.go index 50ade39e..92a78897 100644 --- a/aws-source/adapters/iam-instance-profile_test.go +++ b/aws-source/adapters/iam-instance-profile_test.go @@ -12,28 +12,28 @@ import ( func TestInstanceProfileItemMapper(t *testing.T) { profile := types.InstanceProfile{ - Arn: PtrString("arn:aws:iam::123456789012:instance-profile/webserver"), - CreateDate: PtrTime(time.Now()), - InstanceProfileId: PtrString("AIDACKCEVSQ6C2EXAMPLE"), - InstanceProfileName: PtrString("webserver"), - Path: PtrString("/"), + Arn: new("arn:aws:iam::123456789012:instance-profile/webserver"), + CreateDate: new(time.Now()), + InstanceProfileId: new("AIDACKCEVSQ6C2EXAMPLE"), + InstanceProfileName: new("webserver"), + Path: new("/"), Roles: []types.Role{ { - Arn: PtrString("arn:aws:iam::123456789012:role/webserver"), // link - CreateDate: PtrTime(time.Now()), - Path: PtrString("/"), - RoleId: PtrString("AIDACKCEVSQ6C2EXAMPLE"), - RoleName: PtrString("webserver"), - AssumeRolePolicyDocument: PtrString(`{}`), - Description: PtrString("Allows EC2 instances to call AWS services on your behalf."), - MaxSessionDuration: PtrInt32(3600), + Arn: new("arn:aws:iam::123456789012:role/webserver"), // link + CreateDate: new(time.Now()), + Path: new("/"), + RoleId: new("AIDACKCEVSQ6C2EXAMPLE"), + RoleName: new("webserver"), + AssumeRolePolicyDocument: new(`{}`), + Description: new("Allows EC2 instances to call AWS services on your behalf."), + MaxSessionDuration: new(int32(3600)), PermissionsBoundary: &types.AttachedPermissionsBoundary{ - PermissionsBoundaryArn: PtrString("arn:aws:iam::123456789012:policy/XCompanyBoundaries"), // link + PermissionsBoundaryArn: new("arn:aws:iam::123456789012:policy/XCompanyBoundaries"), // link PermissionsBoundaryType: types.PermissionsBoundaryAttachmentTypePolicy, }, RoleLastUsed: &types.RoleLastUsed{ - LastUsedDate: PtrTime(time.Now()), - Region: PtrString("us-east-1"), + LastUsedDate: new(time.Now()), + Region: new("us-east-1"), }, }, }, diff --git a/aws-source/adapters/iam-policy.go b/aws-source/adapters/iam-policy.go index 5a48ad15..0383d6e0 100644 --- a/aws-source/adapters/iam-policy.go +++ b/aws-source/adapters/iam-policy.go @@ -39,7 +39,7 @@ func policyGetFunc(ctx context.Context, client IAMClient, scope, query string) ( }, } out, err := client.GetPolicy(ctx, &iam.GetPolicyInput{ - PolicyArn: PtrString(a.String()), + PolicyArn: new(a.String()), }) if err != nil { return nil, err diff --git a/aws-source/adapters/iam-policy_test.go b/aws-source/adapters/iam-policy_test.go index a9b85490..f2e4e593 100644 --- a/aws-source/adapters/iam-policy_test.go +++ b/aws-source/adapters/iam-policy_test.go @@ -17,16 +17,16 @@ import ( func (t *TestIAMClient) GetPolicy(ctx context.Context, params *iam.GetPolicyInput, optFns ...func(*iam.Options)) (*iam.GetPolicyOutput, error) { return &iam.GetPolicyOutput{ Policy: &types.Policy{ - PolicyName: PtrString("AWSControlTowerStackSetRolePolicy"), - PolicyId: PtrString("ANPA3VLV2U277MP54R2OV"), - Arn: PtrString("arn:aws:iam::801795385023:policy/service-role/AWSControlTowerStackSetRolePolicy"), - Path: PtrString("/service-role/"), - DefaultVersionId: PtrString("v1"), - AttachmentCount: PtrInt32(1), - PermissionsBoundaryUsageCount: PtrInt32(0), + PolicyName: new("AWSControlTowerStackSetRolePolicy"), + PolicyId: new("ANPA3VLV2U277MP54R2OV"), + Arn: new("arn:aws:iam::801795385023:policy/service-role/AWSControlTowerStackSetRolePolicy"), + Path: new("/service-role/"), + DefaultVersionId: new("v1"), + AttachmentCount: new(int32(1)), + PermissionsBoundaryUsageCount: new(int32(0)), IsAttachable: true, - CreateDate: PtrTime(time.Now()), - UpdateDate: PtrTime(time.Now()), + CreateDate: new(time.Now()), + UpdateDate: new(time.Now()), }, }, nil } @@ -35,20 +35,20 @@ func (t *TestIAMClient) ListEntitiesForPolicy(context.Context, *iam.ListEntities return &iam.ListEntitiesForPolicyOutput{ PolicyGroups: []types.PolicyGroup{ { - GroupId: PtrString("groupId"), - GroupName: PtrString("groupName"), + GroupId: new("groupId"), + GroupName: new("groupName"), }, }, PolicyRoles: []types.PolicyRole{ { - RoleId: PtrString("roleId"), - RoleName: PtrString("roleName"), + RoleId: new("roleId"), + RoleName: new("roleName"), }, }, PolicyUsers: []types.PolicyUser{ { - UserId: PtrString("userId"), - UserName: PtrString("userName"), + UserId: new("userId"), + UserName: new("userName"), }, }, }, nil @@ -58,28 +58,28 @@ func (t *TestIAMClient) ListPolicies(context.Context, *iam.ListPoliciesInput, .. return &iam.ListPoliciesOutput{ Policies: []types.Policy{ { - PolicyName: PtrString("AWSControlTowerAdminPolicy"), - PolicyId: PtrString("ANPA3VLV2U2745H37HTHN"), - Arn: PtrString("arn:aws:iam::801795385023:policy/service-role/AWSControlTowerAdminPolicy"), - Path: PtrString("/service-role/"), - DefaultVersionId: PtrString("v1"), - AttachmentCount: PtrInt32(1), - PermissionsBoundaryUsageCount: PtrInt32(0), + PolicyName: new("AWSControlTowerAdminPolicy"), + PolicyId: new("ANPA3VLV2U2745H37HTHN"), + Arn: new("arn:aws:iam::801795385023:policy/service-role/AWSControlTowerAdminPolicy"), + Path: new("/service-role/"), + DefaultVersionId: new("v1"), + AttachmentCount: new(int32(1)), + PermissionsBoundaryUsageCount: new(int32(0)), IsAttachable: true, - CreateDate: PtrTime(time.Now()), - UpdateDate: PtrTime(time.Now()), + CreateDate: new(time.Now()), + UpdateDate: new(time.Now()), }, { - PolicyName: PtrString("AWSControlTowerCloudTrailRolePolicy"), - PolicyId: PtrString("ANPA3VLV2U27UOP7KSM6I"), - Arn: PtrString("arn:aws:iam::801795385023:policy/service-role/AWSControlTowerCloudTrailRolePolicy"), - Path: PtrString("/service-role/"), - DefaultVersionId: PtrString("v1"), - AttachmentCount: PtrInt32(1), - PermissionsBoundaryUsageCount: PtrInt32(0), + PolicyName: new("AWSControlTowerCloudTrailRolePolicy"), + PolicyId: new("ANPA3VLV2U27UOP7KSM6I"), + Arn: new("arn:aws:iam::801795385023:policy/service-role/AWSControlTowerCloudTrailRolePolicy"), + Path: new("/service-role/"), + DefaultVersionId: new("v1"), + AttachmentCount: new(int32(1)), + PermissionsBoundaryUsageCount: new(int32(0)), IsAttachable: true, - CreateDate: PtrTime(time.Now()), - UpdateDate: PtrTime(time.Now()), + CreateDate: new(time.Now()), + UpdateDate: new(time.Now()), }, }, }, nil @@ -89,8 +89,8 @@ func (t *TestIAMClient) ListPolicyTags(ctx context.Context, params *iam.ListPoli return &iam.ListPolicyTagsOutput{ Tags: []types.Tag{ { - Key: PtrString("foo"), - Value: PtrString("foo"), + Key: new("foo"), + Value: new("foo"), }, }, }, nil @@ -208,7 +208,7 @@ func TestPolicyGetFunc(t *testing.T) { func TestPolicyListTagsFunc(t *testing.T) { tags, err := policyListTagsFunc(context.Background(), &PolicyDetails{ Policy: &types.Policy{ - Arn: PtrString("arn:aws:iam::801795385023:policy/service-role/AWSControlTowerAdminPolicy"), + Arn: new("arn:aws:iam::801795385023:policy/service-role/AWSControlTowerAdminPolicy"), }, }, &TestIAMClient{}) if err != nil { @@ -223,33 +223,33 @@ func TestPolicyListTagsFunc(t *testing.T) { func TestPolicyItemMapper(t *testing.T) { details := &PolicyDetails{ Policy: &types.Policy{ - PolicyName: PtrString("AWSControlTowerAdminPolicy"), - PolicyId: PtrString("ANPA3VLV2U2745H37HTHN"), - Arn: PtrString("arn:aws:iam::801795385023:policy/service-role/AWSControlTowerAdminPolicy"), - Path: PtrString("/service-role/"), - DefaultVersionId: PtrString("v1"), - AttachmentCount: PtrInt32(1), - PermissionsBoundaryUsageCount: PtrInt32(0), + PolicyName: new("AWSControlTowerAdminPolicy"), + PolicyId: new("ANPA3VLV2U2745H37HTHN"), + Arn: new("arn:aws:iam::801795385023:policy/service-role/AWSControlTowerAdminPolicy"), + Path: new("/service-role/"), + DefaultVersionId: new("v1"), + AttachmentCount: new(int32(1)), + PermissionsBoundaryUsageCount: new(int32(0)), IsAttachable: true, - CreateDate: PtrTime(time.Now()), - UpdateDate: PtrTime(time.Now()), + CreateDate: new(time.Now()), + UpdateDate: new(time.Now()), }, PolicyGroups: []types.PolicyGroup{ { - GroupId: PtrString("groupId"), - GroupName: PtrString("groupName"), + GroupId: new("groupId"), + GroupName: new("groupName"), }, }, PolicyRoles: []types.PolicyRole{ { - RoleId: PtrString("roleId"), - RoleName: PtrString("roleName"), + RoleId: new("roleId"), + RoleName: new("roleName"), }, }, PolicyUsers: []types.PolicyUser{ { - UserId: PtrString("userId"), - UserName: PtrString("userName"), + UserId: new("userId"), + UserName: new("userName"), }, }, } diff --git a/aws-source/adapters/iam-role_test.go b/aws-source/adapters/iam-role_test.go index e08d1456..aada9e72 100644 --- a/aws-source/adapters/iam-role_test.go +++ b/aws-source/adapters/iam-role_test.go @@ -19,12 +19,12 @@ import ( func (t *TestIAMClient) GetRole(ctx context.Context, params *iam.GetRoleInput, optFns ...func(*iam.Options)) (*iam.GetRoleOutput, error) { return &iam.GetRoleOutput{ Role: &types.Role{ - Path: PtrString("/service-role/"), - RoleName: PtrString("AWSControlTowerConfigAggregatorRoleForOrganizations"), - RoleId: PtrString("AROA3VLV2U27YSTBFCGCJ"), - Arn: PtrString("arn:aws:iam::801795385023:role/service-role/AWSControlTowerConfigAggregatorRoleForOrganizations"), - CreateDate: PtrTime(time.Now()), - AssumeRolePolicyDocument: PtrString(`{ + Path: new("/service-role/"), + RoleName: new("AWSControlTowerConfigAggregatorRoleForOrganizations"), + RoleId: new("AROA3VLV2U27YSTBFCGCJ"), + Arn: new("arn:aws:iam::801795385023:role/service-role/AWSControlTowerConfigAggregatorRoleForOrganizations"), + CreateDate: new(time.Now()), + AssumeRolePolicyDocument: new(`{ "Version": "2012-10-17", "Statement": [ { @@ -36,7 +36,7 @@ func (t *TestIAMClient) GetRole(ctx context.Context, params *iam.GetRoleInput, o } ] }`), - MaxSessionDuration: PtrInt32(3600), + MaxSessionDuration: new(int32(3600)), }, }, nil } @@ -54,12 +54,12 @@ func (t *TestIAMClient) ListRoles(context.Context, *iam.ListRolesInput, ...func( return &iam.ListRolesOutput{ Roles: []types.Role{ { - Path: PtrString("/service-role/"), - RoleName: PtrString("AWSControlTowerConfigAggregatorRoleForOrganizations"), - RoleId: PtrString("AROA3VLV2U27YSTBFCGCJ"), - Arn: PtrString("arn:aws:iam::801795385023:role/service-role/AWSControlTowerConfigAggregatorRoleForOrganizations"), - CreateDate: PtrTime(time.Now()), - AssumeRolePolicyDocument: PtrString(`{ + Path: new("/service-role/"), + RoleName: new("AWSControlTowerConfigAggregatorRoleForOrganizations"), + RoleId: new("AROA3VLV2U27YSTBFCGCJ"), + Arn: new("arn:aws:iam::801795385023:role/service-role/AWSControlTowerConfigAggregatorRoleForOrganizations"), + CreateDate: new(time.Now()), + AssumeRolePolicyDocument: new(`{ "Version": "2012-10-17", "Statement": [ { @@ -71,7 +71,7 @@ func (t *TestIAMClient) ListRoles(context.Context, *iam.ListRolesInput, ...func( } ] }`), - MaxSessionDuration: PtrInt32(3600), + MaxSessionDuration: new(int32(3600)), }, }, }, nil @@ -81,8 +81,8 @@ func (t *TestIAMClient) ListRoleTags(ctx context.Context, params *iam.ListRoleTa return &iam.ListRoleTagsOutput{ Tags: []types.Tag{ { - Key: PtrString("foo"), - Value: PtrString("bar"), + Key: new("foo"), + Value: new("bar"), }, }, }, nil @@ -91,7 +91,7 @@ func (t *TestIAMClient) ListRoleTags(ctx context.Context, params *iam.ListRoleTa func (t *TestIAMClient) GetRolePolicy(ctx context.Context, params *iam.GetRolePolicyInput, optFns ...func(*iam.Options)) (*iam.GetRolePolicyOutput, error) { return &iam.GetRolePolicyOutput{ PolicyName: params.PolicyName, - PolicyDocument: PtrString(`{ + PolicyDocument: new(`{ "Version": "2012-10-17", "Statement": [ { @@ -110,12 +110,12 @@ func (t *TestIAMClient) ListAttachedRolePolicies(ctx context.Context, params *ia return &iam.ListAttachedRolePoliciesOutput{ AttachedPolicies: []types.AttachedPolicy{ { - PolicyArn: PtrString("arn:aws:iam::aws:policy/AdministratorAccess"), - PolicyName: PtrString("AdministratorAccess"), + PolicyArn: new("arn:aws:iam::aws:policy/AdministratorAccess"), + PolicyName: new("AdministratorAccess"), }, { - PolicyArn: PtrString("arn:aws:iam::aws:policy/AmazonS3FullAccess"), - PolicyName: PtrString("AmazonS3FullAccess"), + PolicyArn: new("arn:aws:iam::aws:policy/AmazonS3FullAccess"), + PolicyName: new("AmazonS3FullAccess"), }, }, }, nil @@ -160,7 +160,7 @@ func TestRoleListFunc(t *testing.T) { func TestRoleListTagsFunc(t *testing.T) { tags, err := roleListTagsFunc(context.Background(), &RoleDetails{ Role: &types.Role{ - Arn: PtrString("arn:aws:iam::801795385023:role/service-role/AWSControlTowerConfigAggregatorRoleForOrganizations"), + Arn: new("arn:aws:iam::801795385023:role/service-role/AWSControlTowerConfigAggregatorRoleForOrganizations"), }, }, &TestIAMClient{}) if err != nil { @@ -193,21 +193,21 @@ func TestRoleItemMapper(t *testing.T) { role := RoleDetails{ Role: &types.Role{ - Path: PtrString("/service-role/"), - RoleName: PtrString("AWSControlTowerConfigAggregatorRoleForOrganizations"), - RoleId: PtrString("AROA3VLV2U27YSTBFCGCJ"), - Arn: PtrString("arn:aws:iam::801795385023:role/service-role/AWSControlTowerConfigAggregatorRoleForOrganizations"), - CreateDate: PtrTime(time.Now()), - AssumeRolePolicyDocument: PtrString(`%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22config.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D`), - MaxSessionDuration: PtrInt32(3600), - Description: PtrString("description"), + Path: new("/service-role/"), + RoleName: new("AWSControlTowerConfigAggregatorRoleForOrganizations"), + RoleId: new("AROA3VLV2U27YSTBFCGCJ"), + Arn: new("arn:aws:iam::801795385023:role/service-role/AWSControlTowerConfigAggregatorRoleForOrganizations"), + CreateDate: new(time.Now()), + AssumeRolePolicyDocument: new(`%7B%22Version%22%3A%222012-10-17%22%2C%22Statement%22%3A%5B%7B%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22config.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D`), + MaxSessionDuration: new(int32(3600)), + Description: new("description"), PermissionsBoundary: &types.AttachedPermissionsBoundary{ - PermissionsBoundaryArn: PtrString("arn:aws:iam::801795385023:role/service-role/AWSControlTowerConfigAggregatorRoleForOrganizations"), + PermissionsBoundaryArn: new("arn:aws:iam::801795385023:role/service-role/AWSControlTowerConfigAggregatorRoleForOrganizations"), PermissionsBoundaryType: types.PermissionsBoundaryAttachmentTypePolicy, }, RoleLastUsed: &types.RoleLastUsed{ - LastUsedDate: PtrTime(time.Now()), - Region: PtrString("us-east-2"), + LastUsedDate: new(time.Now()), + Region: new("us-east-2"), }, }, EmbeddedPolicies: []embeddedPolicy{ @@ -218,8 +218,8 @@ func TestRoleItemMapper(t *testing.T) { }, AttachedPolicies: []types.AttachedPolicy{ { - PolicyArn: PtrString("arn:aws:iam::aws:policy/AdministratorAccess"), - PolicyName: PtrString("AdministratorAccess"), + PolicyArn: new("arn:aws:iam::aws:policy/AdministratorAccess"), + PolicyName: new("AdministratorAccess"), }, }, } diff --git a/aws-source/adapters/iam-user_test.go b/aws-source/adapters/iam-user_test.go index 66c6c8af..8860fcd3 100644 --- a/aws-source/adapters/iam-user_test.go +++ b/aws-source/adapters/iam-user_test.go @@ -21,7 +21,7 @@ func (t *TestIAMClient) ListGroupsForUser(ctx context.Context, params *iam.ListG marker := params.Marker if marker == nil { - marker = PtrString("0") + marker = new("0") } // Get the current page @@ -34,17 +34,17 @@ func (t *TestIAMClient) ListGroupsForUser(ctx context.Context, params *iam.ListG isTruncated = false marker = nil } else { - marker = PtrString(fmt.Sprint(markerInt)) + marker = new(fmt.Sprint(markerInt)) } return &iam.ListGroupsForUserOutput{ Groups: []types.Group{ { - Arn: PtrString("arn:aws:iam::801795385023:Group/something"), - CreateDate: PtrTime(time.Now()), - GroupId: PtrString("id"), - GroupName: PtrString(fmt.Sprintf("group-%v", marker)), - Path: PtrString("/"), + Arn: new("arn:aws:iam::801795385023:Group/something"), + CreateDate: new(time.Now()), + GroupId: new("id"), + GroupName: new(fmt.Sprintf("group-%v", marker)), + Path: new("/"), }, }, IsTruncated: isTruncated, @@ -55,11 +55,11 @@ func (t *TestIAMClient) ListGroupsForUser(ctx context.Context, params *iam.ListG func (t *TestIAMClient) GetUser(ctx context.Context, params *iam.GetUserInput, optFns ...func(*iam.Options)) (*iam.GetUserOutput, error) { return &iam.GetUserOutput{ User: &types.User{ - Path: PtrString("/"), - UserName: PtrString("power-users"), - UserId: PtrString("AGPA3VLV2U27T6SSLJMDS"), - Arn: PtrString("arn:aws:iam::801795385023:User/power-users"), - CreateDate: PtrTime(time.Now()), + Path: new("/"), + UserName: new("power-users"), + UserId: new("AGPA3VLV2U27T6SSLJMDS"), + Arn: new("arn:aws:iam::801795385023:User/power-users"), + CreateDate: new(time.Now()), }, }, nil } @@ -69,7 +69,7 @@ func (t *TestIAMClient) ListUsers(ctx context.Context, params *iam.ListUsersInpu marker := params.Marker if marker == nil { - marker = PtrString("0") + marker = new("0") } // Get the current page @@ -82,17 +82,17 @@ func (t *TestIAMClient) ListUsers(ctx context.Context, params *iam.ListUsersInpu isTruncated = false marker = nil } else { - marker = PtrString(fmt.Sprint(markerInt)) + marker = new(fmt.Sprint(markerInt)) } return &iam.ListUsersOutput{ Users: []types.User{ { - Path: PtrString("/"), - UserName: PtrString(fmt.Sprintf("user-%v", marker)), - UserId: PtrString("AGPA3VLV2U27T6SSLJMDS"), - Arn: PtrString("arn:aws:iam::801795385023:User/power-users"), - CreateDate: PtrTime(time.Now()), + Path: new("/"), + UserName: new(fmt.Sprintf("user-%v", marker)), + UserId: new("AGPA3VLV2U27T6SSLJMDS"), + Arn: new("arn:aws:iam::801795385023:User/power-users"), + CreateDate: new(time.Now()), }, }, IsTruncated: isTruncated, @@ -104,8 +104,8 @@ func (t *TestIAMClient) ListUserTags(context.Context, *iam.ListUserTagsInput, .. return &iam.ListUserTagsOutput{ Tags: []types.Tag{ { - Key: PtrString("foo"), - Value: PtrString("bar"), + Key: new("foo"), + Value: new("bar"), }, }, IsTruncated: false, @@ -114,7 +114,7 @@ func (t *TestIAMClient) ListUserTags(context.Context, *iam.ListUserTagsInput, .. } func TestGetUserGroups(t *testing.T) { - groups, err := getUserGroups(context.Background(), &TestIAMClient{}, PtrString("foo")) + groups, err := getUserGroups(context.Background(), &TestIAMClient{}, new("foo")) if err != nil { t.Error(err) } @@ -168,7 +168,7 @@ func TestUserListFunc(t *testing.T) { func TestUserListTagsFunc(t *testing.T) { tags, err := userListTagsFunc(context.Background(), &UserDetails{ User: &types.User{ - UserName: PtrString("foo"), + UserName: new("foo"), }, }, &TestIAMClient{}) if err != nil { @@ -183,19 +183,19 @@ func TestUserListTagsFunc(t *testing.T) { func TestUserItemMapper(t *testing.T) { details := UserDetails{ User: &types.User{ - Path: PtrString("/"), - UserName: PtrString("power-users"), - UserId: PtrString("AGPA3VLV2U27T6SSLJMDS"), - Arn: PtrString("arn:aws:iam::801795385023:User/power-users"), - CreateDate: PtrTime(time.Now()), + Path: new("/"), + UserName: new("power-users"), + UserId: new("AGPA3VLV2U27T6SSLJMDS"), + Arn: new("arn:aws:iam::801795385023:User/power-users"), + CreateDate: new(time.Now()), }, UserGroups: []types.Group{ { - Arn: PtrString("arn:aws:iam::801795385023:Group/something"), - CreateDate: PtrTime(time.Now()), - GroupId: PtrString("id"), - GroupName: PtrString("name"), - Path: PtrString("/"), + Arn: new("arn:aws:iam::801795385023:Group/something"), + CreateDate: new(time.Now()), + GroupId: new("id"), + GroupName: new("name"), + Path: new("/"), }, }, } diff --git a/aws-source/adapters/integration/apigateway/create.go b/aws-source/adapters/integration/apigateway/create.go index 429d2c68..a152466c 100644 --- a/aws-source/adapters/integration/apigateway/create.go +++ b/aws-source/adapters/integration/apigateway/create.go @@ -9,7 +9,6 @@ import ( "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/aws/aws-sdk-go-v2/service/apigateway/types" - "github.com/overmindtech/cli/aws-source/adapters" "github.com/overmindtech/cli/aws-source/adapters/integration" ) @@ -30,8 +29,8 @@ func createRestAPI(ctx context.Context, logger *slog.Logger, client *apigateway. } result, err := client.CreateRestApi(ctx, &apigateway.CreateRestApiInput{ - Name: adapters.PtrString(integration.ResourceName(integration.APIGateway, restAPISrc, testID)), - Description: adapters.PtrString("Test Rest API"), + Name: new(integration.ResourceName(integration.APIGateway, restAPISrc, testID)), + Description: new("Test Rest API"), Tags: resourceTags(restAPISrc, testID), }) if err != nil { @@ -60,7 +59,7 @@ func createResource(ctx context.Context, logger *slog.Logger, client *apigateway result, err := client.CreateResource(ctx, &apigateway.CreateResourceInput{ RestApiId: restAPIID, ParentId: parentID, - PathPart: adapters.PtrString(cleanPath(path)), + PathPart: new(cleanPath(path)), }) if err != nil { return nil, err @@ -97,8 +96,8 @@ func createMethod(ctx context.Context, logger *slog.Logger, client *apigateway.C _, err = client.PutMethod(ctx, &apigateway.PutMethodInput{ RestApiId: restAPIID, ResourceId: resourceID, - HttpMethod: adapters.PtrString(method), - AuthorizationType: adapters.PtrString("NONE"), + HttpMethod: new(method), + AuthorizationType: new("NONE"), }) if err != nil { return err @@ -126,8 +125,8 @@ func createMethodResponse(ctx context.Context, logger *slog.Logger, client *apig _, err = client.PutMethodResponse(ctx, &apigateway.PutMethodResponseInput{ RestApiId: restAPIID, ResourceId: resourceID, - HttpMethod: adapters.PtrString(method), - StatusCode: adapters.PtrString(statusCode), + HttpMethod: new(method), + StatusCode: new(statusCode), ResponseModels: map[string]string{ "application/json": "Empty", }, @@ -161,7 +160,7 @@ func createIntegration(ctx context.Context, logger *slog.Logger, client *apigate _, err = client.PutIntegration(ctx, &apigateway.PutIntegrationInput{ RestApiId: restAPIID, ResourceId: resourceID, - HttpMethod: adapters.PtrString(method), + HttpMethod: new(method), Type: "MOCK", }) if err != nil { @@ -188,7 +187,7 @@ func createAPIKey(ctx context.Context, logger *slog.Logger, client *apigateway.C } _, err = client.CreateApiKey(ctx, &apigateway.CreateApiKeyInput{ - Name: adapters.PtrString(integration.ResourceName(integration.APIGateway, apiKeySrc, testID)), + Name: new(integration.ResourceName(integration.APIGateway, apiKeySrc, testID)), Tags: resourceTags(apiKeySrc, testID), Enabled: true, }) @@ -218,10 +217,10 @@ func createAuthorizer(ctx context.Context, logger *slog.Logger, client *apigatew identitySource := "method.request.header.Authorization" _, err = client.CreateAuthorizer(ctx, &apigateway.CreateAuthorizerInput{ RestApiId: &restAPIID, - Name: adapters.PtrString(integration.ResourceName(integration.APIGateway, authorizerSrc, testID)), + Name: new(integration.ResourceName(integration.APIGateway, authorizerSrc, testID)), Type: types.AuthorizerTypeToken, IdentitySource: &identitySource, - AuthorizerUri: adapters.PtrString("arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:auth-function/invocations"), + AuthorizerUri: new("arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:auth-function/invocations"), }) if err != nil { return err @@ -248,7 +247,7 @@ func createDeployment(ctx context.Context, logger *slog.Logger, client *apigatew resp, err := client.CreateDeployment(ctx, &apigateway.CreateDeploymentInput{ RestApiId: &restAPIID, - Description: adapters.PtrString("test-deployment"), + Description: new("test-deployment"), }) if err != nil { return nil, err @@ -307,8 +306,8 @@ func createModel(ctx context.Context, logger *slog.Logger, client *apigateway.Cl _, err = client.CreateModel(ctx, &apigateway.CreateModelInput{ RestApiId: &restAPIID, Name: &modelName, - Schema: adapters.PtrString("{}"), - ContentType: adapters.PtrString("application/json"), + Schema: new("{}"), + ContentType: new("application/json"), }) if err != nil { return err diff --git a/aws-source/adapters/integration/apigateway/delete.go b/aws-source/adapters/integration/apigateway/delete.go index c41b285b..e5ec7f11 100644 --- a/aws-source/adapters/integration/apigateway/delete.go +++ b/aws-source/adapters/integration/apigateway/delete.go @@ -4,13 +4,11 @@ import ( "context" "github.com/aws/aws-sdk-go-v2/service/apigateway" - - "github.com/overmindtech/cli/aws-source/adapters" ) func deleteRestAPI(ctx context.Context, client *apigateway.Client, restAPIID string) error { _, err := client.DeleteRestApi(ctx, &apigateway.DeleteRestApiInput{ - RestApiId: adapters.PtrString(restAPIID), + RestApiId: new(restAPIID), }) return err diff --git a/aws-source/adapters/integration/ec2-transit-gateway/setup.go b/aws-source/adapters/integration/ec2-transit-gateway/setup.go index ec86afd7..a32305ef 100644 --- a/aws-source/adapters/integration/ec2-transit-gateway/setup.go +++ b/aws-source/adapters/integration/ec2-transit-gateway/setup.go @@ -19,7 +19,7 @@ const integrationTestName = "integration-test" // Package-level state set by Setup and used by tests and Teardown. var ( - createdTransitGatewayID string + createdTransitGatewayID string createdRouteTableID string createdVpcID string createdSubnetID string @@ -43,14 +43,14 @@ func Setup(t *testing.T) { func setup(ctx context.Context, logger *slog.Logger, client *ec2.Client) error { out, err := client.CreateTransitGateway(ctx, &ec2.CreateTransitGatewayInput{ - Description: ptr("Overmind " + integrationTestName), + Description: new("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)}, + {Key: new(integration.TagTestKey), Value: new(integration.TagTestValue)}, + {Key: new(integration.TagTestIDKey), Value: new(integrationTestName)}, + {Key: new("Name"), Value: new(integrationTestName)}, }, }, }, @@ -99,7 +99,7 @@ func setup(ctx context.Context, logger *slog.Logger, client *ec2.Client) error { // 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}}, + {Name: new("transit-gateway-id"), Values: []string{tgwID}}, }, }) if err != nil { @@ -118,14 +118,14 @@ func setup(ctx context.Context, logger *slog.Logger, client *ec2.Client) error { // 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"), + CidrBlock: new("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)}, + {Key: new(integration.TagTestKey), Value: new(integration.TagTestValue)}, + {Key: new(integration.TagTestIDKey), Value: new(integrationTestName)}, + {Key: new("Name"), Value: new(integrationTestName)}, }, }, }, @@ -142,7 +142,7 @@ func setup(ctx context.Context, logger *slog.Logger, client *ec2.Client) error { // Pick one AZ for the subnet. azOut, err := client.DescribeAvailabilityZones(ctx, &ec2.DescribeAvailabilityZonesInput{ Filters: []types.Filter{ - {Name: ptr("state"), Values: []string{"available"}}, + {Name: new("state"), Values: []string{"available"}}, }, }) if err != nil || len(azOut.AvailabilityZones) == 0 { @@ -152,15 +152,15 @@ func setup(ctx context.Context, logger *slog.Logger, client *ec2.Client) error { subOut, err := client.CreateSubnet(ctx, &ec2.CreateSubnetInput{ VpcId: &createdVpcID, - CidrBlock: ptr("10.99.1.0/24"), + CidrBlock: new("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)}, + {Key: new(integration.TagTestKey), Value: new(integration.TagTestValue)}, + {Key: new(integration.TagTestIDKey), Value: new(integrationTestName)}, + {Key: new("Name"), Value: new(integrationTestName)}, }, }, }, @@ -182,9 +182,9 @@ func setup(ctx context.Context, logger *slog.Logger, client *ec2.Client) error { { 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)}, + {Key: new(integration.TagTestKey), Value: new(integration.TagTestValue)}, + {Key: new(integration.TagTestIDKey), Value: new(integrationTestName)}, + {Key: new("Name"), Value: new(integrationTestName)}, }, }, }, @@ -239,5 +239,3 @@ func setup(ctx context.Context, logger *slog.Logger, client *ec2.Client) error { 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 index 61e94270..fd73d71d 100644 --- a/aws-source/adapters/integration/ec2-transit-gateway/teardown.go +++ b/aws-source/adapters/integration/ec2-transit-gateway/teardown.go @@ -15,8 +15,8 @@ import ( // 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}}, + {Name: new("tag:" + integration.TagTestKey), Values: []string{integration.TagTestValue}}, + {Name: new("tag:" + integration.TagTestIDKey), Values: []string{integrationTestName}}, } } @@ -81,7 +81,7 @@ func teardown(ctx context.Context, logger *slog.Logger, client *ec2.Client) erro // 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}}}, + Filters: []types.Filter{{Name: new("transit-gateway-id"), Values: []string{tgwID}}}, }) if err != nil { return err @@ -103,7 +103,7 @@ func teardown(ctx context.Context, logger *slog.Logger, client *ec2.Client) erro // 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}}}, + Filters: []types.Filter{{Name: new("transit-gateway-id"), Values: []string{tgwID}}}, }) if err != nil { return err diff --git a/aws-source/adapters/integration/kms/create.go b/aws-source/adapters/integration/kms/create.go index 9f994485..642b18ad 100644 --- a/aws-source/adapters/integration/kms/create.go +++ b/aws-source/adapters/integration/kms/create.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log/slog" + "slices" "github.com/aws/aws-sdk-go-v2/service/kms" "github.com/aws/aws-sdk-go-v2/service/kms/types" @@ -48,11 +49,9 @@ func createAlias(ctx context.Context, logger *slog.Logger, client *kms.Client, k } } - for _, aName := range aliasNames { - if aName == aliasName { - logger.InfoContext(ctx, "KMS alias already exists", "alias", aliasName, "keyID", keyID) - return nil - } + if slices.Contains(aliasNames, aliasName) { + logger.InfoContext(ctx, "KMS alias already exists", "alias", aliasName, "keyID", keyID) + return nil } _, err = client.CreateAlias(ctx, &kms.CreateAliasInput{ diff --git a/aws-source/adapters/integration/kms/kms_test.go b/aws-source/adapters/integration/kms/kms_test.go index 750a7730..626c33db 100644 --- a/aws-source/adapters/integration/kms/kms_test.go +++ b/aws-source/adapters/integration/kms/kms_test.go @@ -148,7 +148,7 @@ func KMS(t *testing.T) { } // Get the alias for this key - var aliasUniqueAttributeValue interface{} + var aliasUniqueAttributeValue any for _, alias := range sdpListAliases { // Check if the alias is for the key diff --git a/aws-source/adapters/integration/ssm/main_test.go b/aws-source/adapters/integration/ssm/main_test.go index ab13915c..81492ce8 100644 --- a/aws-source/adapters/integration/ssm/main_test.go +++ b/aws-source/adapters/integration/ssm/main_test.go @@ -184,10 +184,7 @@ func TestIntegrationSSM(t *testing.T) { // Delete parameters in batches of 100 for i := 0; i < len(output.Parameters); i += 100 { - end := i + 100 - if end > len(output.Parameters) { - end = len(output.Parameters) - } + end := min(i+100, len(output.Parameters)) batch := output.Parameters[i:end] names := make([]string, len(batch)) diff --git a/aws-source/adapters/kms-alias_test.go b/aws-source/adapters/kms-alias_test.go index 3e99443a..e4e86ba1 100644 --- a/aws-source/adapters/kms-alias_test.go +++ b/aws-source/adapters/kms-alias_test.go @@ -15,11 +15,11 @@ func TestAliasOutputMapper(t *testing.T) { output := &kms.ListAliasesOutput{ Aliases: []types.AliasListEntry{ { - AliasName: PtrString("alias/test-key"), - TargetKeyId: PtrString("cf68415c-f4ae-48f2-87a7-3b52ce"), - AliasArn: PtrString("arn:aws:kms:us-west-2:123456789012:alias/test-key"), - CreationDate: PtrTime(time.Now()), - LastUpdatedDate: PtrTime(time.Now()), + AliasName: new("alias/test-key"), + TargetKeyId: new("cf68415c-f4ae-48f2-87a7-3b52ce"), + AliasArn: new("arn:aws:kms:us-west-2:123456789012:alias/test-key"), + CreationDate: new(time.Now()), + LastUpdatedDate: new(time.Now()), }, }, } diff --git a/aws-source/adapters/kms-custom-key-store_test.go b/aws-source/adapters/kms-custom-key-store_test.go index 19c2d834..849b740d 100644 --- a/aws-source/adapters/kms-custom-key-store_test.go +++ b/aws-source/adapters/kms-custom-key-store_test.go @@ -17,12 +17,12 @@ func TestCustomKeyStoreOutputMapper(t *testing.T) { output := &kms.DescribeCustomKeyStoresOutput{ CustomKeyStores: []types.CustomKeyStoresListEntry{ { - CustomKeyStoreId: PtrString("custom-key-store-1"), - CreationDate: PtrTime(time.Now()), - CloudHsmClusterId: PtrString("cloud-hsm-cluster-1"), + CustomKeyStoreId: new("custom-key-store-1"), + CreationDate: new(time.Now()), + CloudHsmClusterId: new("cloud-hsm-cluster-1"), ConnectionState: types.ConnectionStateTypeConnected, - TrustAnchorCertificate: PtrString("-----BEGIN CERTIFICATE-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwJ1z\n-----END CERTIFICATE-----"), - CustomKeyStoreName: PtrString("key-store-1"), + TrustAnchorCertificate: new("-----BEGIN CERTIFICATE-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwJ1z\n-----END CERTIFICATE-----"), + CustomKeyStoreName: new("key-store-1"), }, }, } @@ -82,7 +82,7 @@ func TestHealthState(t *testing.T) { output: &kms.DescribeCustomKeyStoresOutput{ CustomKeyStores: []types.CustomKeyStoresListEntry{ { - CustomKeyStoreId: PtrString("custom-key-store-1"), + CustomKeyStoreId: new("custom-key-store-1"), ConnectionState: types.ConnectionStateTypeConnected, }, }, @@ -94,7 +94,7 @@ func TestHealthState(t *testing.T) { output: &kms.DescribeCustomKeyStoresOutput{ CustomKeyStores: []types.CustomKeyStoresListEntry{ { - CustomKeyStoreId: PtrString("custom-key-store-1"), + CustomKeyStoreId: new("custom-key-store-1"), ConnectionState: types.ConnectionStateTypeConnecting, }, }, @@ -106,7 +106,7 @@ func TestHealthState(t *testing.T) { output: &kms.DescribeCustomKeyStoresOutput{ CustomKeyStores: []types.CustomKeyStoresListEntry{ { - CustomKeyStoreId: PtrString("custom-key-store-1"), + CustomKeyStoreId: new("custom-key-store-1"), ConnectionState: types.ConnectionStateTypeDisconnected, }, }, @@ -118,7 +118,7 @@ func TestHealthState(t *testing.T) { output: &kms.DescribeCustomKeyStoresOutput{ CustomKeyStores: []types.CustomKeyStoresListEntry{ { - CustomKeyStoreId: PtrString("custom-key-store-1"), + CustomKeyStoreId: new("custom-key-store-1"), ConnectionState: types.ConnectionStateTypeFailed, }, }, @@ -130,7 +130,7 @@ func TestHealthState(t *testing.T) { output: &kms.DescribeCustomKeyStoresOutput{ CustomKeyStores: []types.CustomKeyStoresListEntry{ { - CustomKeyStoreId: PtrString("custom-key-store-1"), + CustomKeyStoreId: new("custom-key-store-1"), ConnectionState: "unknown-state", }, }, diff --git a/aws-source/adapters/kms-grant.go b/aws-source/adapters/kms-grant.go index 2d3e90cc..1804cdb9 100644 --- a/aws-source/adapters/kms-grant.go +++ b/aws-source/adapters/kms-grant.go @@ -173,7 +173,7 @@ func NewKMSGrantAdapter(client *kms.Client, accountID string, region string, cac AccountID: accountID, Region: region, AdapterMetadata: grantAdapterMetadata, - cache: cache, + cache: cache, DescribeFunc: func(ctx context.Context, client *kms.Client, input *kms.ListGrantsInput) (*kms.ListGrantsOutput, error) { return client.ListGrants(ctx, input) }, @@ -189,8 +189,8 @@ func NewKMSGrantAdapter(client *kms.Client, accountID string, region string, cac } return &kms.ListGrantsInput{ - KeyId: &tmp[0], // keyID - GrantId: PtrString(strings.Join(tmp[1:], "/")), // grantId + KeyId: &tmp[0], // keyID + GrantId: new(strings.Join(tmp[1:], "/")), // grantId }, nil }, UseListForGet: true, diff --git a/aws-source/adapters/kms-grant_test.go b/aws-source/adapters/kms-grant_test.go index c2f8dbf9..d824cd10 100644 --- a/aws-source/adapters/kms-grant_test.go +++ b/aws-source/adapters/kms-grant_test.go @@ -130,14 +130,14 @@ func TestGrantOutputMapper(t *testing.T) { "aws:dynamodb:tableName": "Services", }, }, - IssuingAccount: PtrString("arn:aws:iam::123456789012:root"), - Name: PtrString("8276b9a6-6cf0-46f1-b2f0-7993a7f8c89a"), + IssuingAccount: new("arn:aws:iam::123456789012:root"), + Name: new("8276b9a6-6cf0-46f1-b2f0-7993a7f8c89a"), Operations: []types.GrantOperation{"Decrypt", "Encrypt", "GenerateDataKey", "ReEncryptFrom", "ReEncryptTo", "RetireGrant", "DescribeKey"}, - GrantId: PtrString("1667b97d27cf748cf05b487217dd4179526c949d14fb3903858e25193253fe59"), - KeyId: PtrString("arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"), - RetiringPrincipal: PtrString("arn:aws:iam::account:role/role-name-with-path"), - GranteePrincipal: PtrString("arn:aws:iam::account:user/user-name-with-path"), - CreationDate: PtrTime(time.Now()), + GrantId: new("1667b97d27cf748cf05b487217dd4179526c949d14fb3903858e25193253fe59"), + KeyId: new("arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"), + RetiringPrincipal: new("arn:aws:iam::account:role/role-name-with-path"), + GranteePrincipal: new("arn:aws:iam::account:user/user-name-with-path"), + CreationDate: new(time.Now()), }, }, } @@ -197,15 +197,15 @@ func TestGrantOutputMapperWithServicePrincipal(t *testing.T) { "aws:dynamodb:tableName": "Services", }, }, - IssuingAccount: PtrString("arn:aws:iam::123456789012:root"), - Name: PtrString("8276b9a6-6cf0-46f1-b2f0-7993a7f8c89a"), + IssuingAccount: new("arn:aws:iam::123456789012:root"), + Name: new("8276b9a6-6cf0-46f1-b2f0-7993a7f8c89a"), Operations: []types.GrantOperation{"Decrypt", "Encrypt"}, - GrantId: PtrString("1667b97d27cf748cf05b487217dd4179526c949d14fb3903858e25193253fe59"), - KeyId: PtrString("arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"), + GrantId: new("1667b97d27cf748cf05b487217dd4179526c949d14fb3903858e25193253fe59"), + KeyId: new("arn:aws:kms:us-west-2:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab"), // These are service principals, not ARNs - they should be skipped - RetiringPrincipal: PtrString("dynamodb.us-west-2.amazonaws.com"), - GranteePrincipal: PtrString("rds.eu-west-2.amazonaws.com"), - CreationDate: PtrTime(time.Now()), + RetiringPrincipal: new("dynamodb.us-west-2.amazonaws.com"), + GranteePrincipal: new("rds.eu-west-2.amazonaws.com"), + CreationDate: new(time.Now()), }, }, } diff --git a/aws-source/adapters/kms-key-policy_test.go b/aws-source/adapters/kms-key-policy_test.go index be46f433..f0b98a2e 100644 --- a/aws-source/adapters/kms-key-policy_test.go +++ b/aws-source/adapters/kms-key-policy_test.go @@ -43,7 +43,7 @@ type mockKeyPolicyClient struct{} func (m *mockKeyPolicyClient) GetKeyPolicy(ctx context.Context, params *kms.GetKeyPolicyInput, optFns ...func(*kms.Options)) (*kms.GetKeyPolicyOutput, error) { return &kms.GetKeyPolicyOutput{ - Policy: PtrString(`{ + Policy: new(`{ "Version" : "2012-10-17", "Id" : "key-default-1", "Statement" : [ @@ -67,7 +67,7 @@ func (m *mockKeyPolicyClient) GetKeyPolicy(ctx context.Context, params *kms.GetK } ] }`), - PolicyName: PtrString("default"), + PolicyName: new("default"), }, nil } @@ -82,7 +82,7 @@ func TestGetKeyPolicyFunc(t *testing.T) { cli := &mockKeyPolicyClient{} item, err := getKeyPolicyFunc(ctx, cli, "scope", &kms.GetKeyPolicyInput{ - KeyId: PtrString("1234abcd-12ab-34cd-56ef-1234567890ab"), + KeyId: new("1234abcd-12ab-34cd-56ef-1234567890ab"), }) if err != nil { t.Fatal(err) diff --git a/aws-source/adapters/kms-key_test.go b/aws-source/adapters/kms-key_test.go index 9927110e..03e0f887 100644 --- a/aws-source/adapters/kms-key_test.go +++ b/aws-source/adapters/kms-key_test.go @@ -15,12 +15,12 @@ type kmsTestClient struct{} func (t kmsTestClient) DescribeKey(ctx context.Context, params *kms.DescribeKeyInput, optFns ...func(*kms.Options)) (*kms.DescribeKeyOutput, error) { return &kms.DescribeKeyOutput{ KeyMetadata: &types.KeyMetadata{ - AWSAccountId: PtrString("846764612917"), - KeyId: PtrString("b8a9477d-836c-491f-857e-07937918959b"), - Arn: PtrString("arn:aws:kms:us-west-2:846764612917:key/b8a9477d-836c-491f-857e-07937918959b"), - CreationDate: PtrTime(time.Date(2017, 6, 30, 21, 44, 32, 140000000, time.UTC)), + AWSAccountId: new("846764612917"), + KeyId: new("b8a9477d-836c-491f-857e-07937918959b"), + Arn: new("arn:aws:kms:us-west-2:846764612917:key/b8a9477d-836c-491f-857e-07937918959b"), + CreationDate: new(time.Date(2017, 6, 30, 21, 44, 32, 140000000, time.UTC)), Enabled: true, - Description: PtrString("Default KMS key that protects my S3 objects when no other key is defined"), + Description: new("Default KMS key that protects my S3 objects when no other key is defined"), KeyUsage: types.KeyUsageTypeEncryptDecrypt, KeyState: types.KeyStateEnabled, Origin: types.OriginTypeAwsKms, @@ -37,16 +37,16 @@ func (t kmsTestClient) ListKeys(context.Context, *kms.ListKeysInput, ...func(*km return &kms.ListKeysOutput{ Keys: []types.KeyListEntry{ { - KeyArn: PtrString("arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"), - KeyId: PtrString("1234abcd-12ab-34cd-56ef-1234567890ab"), + KeyArn: new("arn:aws:kms:us-west-2:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"), + KeyId: new("1234abcd-12ab-34cd-56ef-1234567890ab"), }, { - KeyArn: PtrString("arn:aws:kms:us-west-2:111122223333:key/0987dcba-09fe-87dc-65ba-ab0987654321"), - KeyId: PtrString("0987dcba-09fe-87dc-65ba-ab0987654321"), + KeyArn: new("arn:aws:kms:us-west-2:111122223333:key/0987dcba-09fe-87dc-65ba-ab0987654321"), + KeyId: new("0987dcba-09fe-87dc-65ba-ab0987654321"), }, { - KeyArn: PtrString("arn:aws:kms:us-east-2:111122223333:key/1a2b3c4d-5e6f-1a2b-3c4d-5e6f1a2b3c4d"), - KeyId: PtrString("1a2b3c4d-5e6f-1a2b-3c4d-5e6f1a2b3c4d"), + KeyArn: new("arn:aws:kms:us-east-2:111122223333:key/1a2b3c4d-5e6f-1a2b-3c4d-5e6f1a2b3c4d"), + KeyId: new("1a2b3c4d-5e6f-1a2b-3c4d-5e6f1a2b3c4d"), }, }, }, nil @@ -56,16 +56,16 @@ func (t kmsTestClient) ListResourceTags(context.Context, *kms.ListResourceTagsIn return &kms.ListResourceTagsOutput{ Tags: []types.Tag{ { - TagKey: PtrString("Dept"), - TagValue: PtrString("IT"), + TagKey: new("Dept"), + TagValue: new("IT"), }, { - TagKey: PtrString("Purpose"), - TagValue: PtrString("Test"), + TagKey: new("Purpose"), + TagValue: new("Test"), }, { - TagKey: PtrString("Name"), - TagValue: PtrString("Test"), + TagKey: new("Name"), + TagValue: new("Test"), }, }, }, nil @@ -76,7 +76,7 @@ func TestKMSGetFunc(t *testing.T) { cli := kmsTestClient{} item, err := kmsKeyGetFunc(ctx, cli, "scope", &kms.DescribeKeyInput{ - KeyId: PtrString("1234abcd-12ab-34cd-56ef-1234567890ab"), + KeyId: new("1234abcd-12ab-34cd-56ef-1234567890ab"), }) if err != nil { t.Fatal(err) diff --git a/aws-source/adapters/lambda-event-source-mapping_test.go b/aws-source/adapters/lambda-event-source-mapping_test.go index 9a9206ad..0959b810 100644 --- a/aws-source/adapters/lambda-event-source-mapping_test.go +++ b/aws-source/adapters/lambda-event-source-mapping_test.go @@ -15,22 +15,22 @@ type TestLambdaEventSourceMappingClient struct{} func (t *TestLambdaEventSourceMappingClient) ListEventSourceMappings(ctx context.Context, params *lambda.ListEventSourceMappingsInput, optFns ...func(*lambda.Options)) (*lambda.ListEventSourceMappingsOutput, error) { allMappings := []types.EventSourceMappingConfiguration{ { - UUID: stringPtr("test-uuid-1"), - FunctionArn: stringPtr("arn:aws:lambda:us-east-1:123456789012:function:test-function"), - EventSourceArn: stringPtr("arn:aws:sqs:us-east-1:123456789012:test-queue"), - State: stringPtr("Enabled"), + UUID: new("test-uuid-1"), + FunctionArn: new("arn:aws:lambda:us-east-1:123456789012:function:test-function"), + EventSourceArn: new("arn:aws:sqs:us-east-1:123456789012:test-queue"), + State: new("Enabled"), }, { - UUID: stringPtr("test-uuid-2"), - FunctionArn: stringPtr("arn:aws:lambda:us-east-1:123456789012:function:test-function-2"), - EventSourceArn: stringPtr("arn:aws:dynamodb:us-east-1:123456789012:table/test-table"), - State: stringPtr("Creating"), + UUID: new("test-uuid-2"), + FunctionArn: new("arn:aws:lambda:us-east-1:123456789012:function:test-function-2"), + EventSourceArn: new("arn:aws:dynamodb:us-east-1:123456789012:table/test-table"), + State: new("Creating"), }, { - UUID: stringPtr("test-uuid-3"), - FunctionArn: stringPtr("arn:aws:lambda:us-east-1:123456789012:function:test-function-3"), - EventSourceArn: stringPtr("arn:aws:rds:us-east-1:123456789012:cluster:test-docdb-cluster"), - State: stringPtr("Enabled"), + UUID: new("test-uuid-3"), + FunctionArn: new("arn:aws:lambda:us-east-1:123456789012:function:test-function-3"), + EventSourceArn: new("arn:aws:rds:us-east-1:123456789012:cluster:test-docdb-cluster"), + State: new("Enabled"), }, } @@ -60,34 +60,30 @@ func (t *TestLambdaEventSourceMappingClient) GetEventSourceMapping(ctx context.C switch *params.UUID { case "test-uuid-1": return &lambda.GetEventSourceMappingOutput{ - UUID: stringPtr("test-uuid-1"), - FunctionArn: stringPtr("arn:aws:lambda:us-east-1:123456789012:function:test-function"), - EventSourceArn: stringPtr("arn:aws:sqs:us-east-1:123456789012:test-queue"), - State: stringPtr("Enabled"), + UUID: new("test-uuid-1"), + FunctionArn: new("arn:aws:lambda:us-east-1:123456789012:function:test-function"), + EventSourceArn: new("arn:aws:sqs:us-east-1:123456789012:test-queue"), + State: new("Enabled"), }, nil case "test-uuid-2": return &lambda.GetEventSourceMappingOutput{ - UUID: stringPtr("test-uuid-2"), - FunctionArn: stringPtr("arn:aws:lambda:us-east-1:123456789012:function:test-function-2"), - EventSourceArn: stringPtr("arn:aws:dynamodb:us-east-1:123456789012:table/test-table"), - State: stringPtr("Creating"), + UUID: new("test-uuid-2"), + FunctionArn: new("arn:aws:lambda:us-east-1:123456789012:function:test-function-2"), + EventSourceArn: new("arn:aws:dynamodb:us-east-1:123456789012:table/test-table"), + State: new("Creating"), }, nil case "test-uuid-3": return &lambda.GetEventSourceMappingOutput{ - UUID: stringPtr("test-uuid-3"), - FunctionArn: stringPtr("arn:aws:lambda:us-east-1:123456789012:function:test-function-3"), - EventSourceArn: stringPtr("arn:aws:rds:us-east-1:123456789012:cluster:test-docdb-cluster"), - State: stringPtr("Enabled"), + UUID: new("test-uuid-3"), + FunctionArn: new("arn:aws:lambda:us-east-1:123456789012:function:test-function-3"), + EventSourceArn: new("arn:aws:rds:us-east-1:123456789012:cluster:test-docdb-cluster"), + State: new("Enabled"), }, nil default: return nil, &types.ResourceNotFoundException{} } } -func stringPtr(s string) *string { - return &s -} - func TestLambdaEventSourceMappingAdapter(t *testing.T) { adapter := NewLambdaEventSourceMappingAdapter(&TestLambdaEventSourceMappingClient{}, "123456789012", "us-east-1", sdpcache.NewNoOpCache()) @@ -150,10 +146,10 @@ func TestLambdaEventSourceMappingItemMapper(t *testing.T) { // Test mapping with SQS event source awsItem := &types.EventSourceMappingConfiguration{ - UUID: stringPtr("test-uuid-1"), - FunctionArn: stringPtr("arn:aws:lambda:us-east-1:123456789012:function:test-function"), - EventSourceArn: stringPtr("arn:aws:sqs:us-east-1:123456789012:test-queue"), - State: stringPtr("Enabled"), + UUID: new("test-uuid-1"), + FunctionArn: new("arn:aws:lambda:us-east-1:123456789012:function:test-function"), + EventSourceArn: new("arn:aws:sqs:us-east-1:123456789012:test-queue"), + State: new("Enabled"), } item, err := adapter.ItemMapper("test-uuid-1", "123456789012.us-east-1", awsItem) @@ -213,10 +209,10 @@ func TestLambdaEventSourceMappingItemMapperWithDynamoDB(t *testing.T) { // Test mapping with DynamoDB event source awsItem := &types.EventSourceMappingConfiguration{ - UUID: stringPtr("test-uuid-2"), - FunctionArn: stringPtr("arn:aws:lambda:us-east-1:123456789012:function:test-function-2"), - EventSourceArn: stringPtr("arn:aws:dynamodb:us-east-1:123456789012:table/test-table"), - State: stringPtr("Creating"), + UUID: new("test-uuid-2"), + FunctionArn: new("arn:aws:lambda:us-east-1:123456789012:function:test-function-2"), + EventSourceArn: new("arn:aws:dynamodb:us-east-1:123456789012:table/test-table"), + State: new("Creating"), } item, err := adapter.ItemMapper("test-uuid-2", "123456789012.us-east-1", awsItem) @@ -246,10 +242,10 @@ func TestLambdaEventSourceMappingItemMapperWithRDS(t *testing.T) { // Test mapping with RDS/DocumentDB event source awsItem := &types.EventSourceMappingConfiguration{ - UUID: stringPtr("test-uuid-3"), - FunctionArn: stringPtr("arn:aws:lambda:us-east-1:123456789012:function:test-function-3"), - EventSourceArn: stringPtr("arn:aws:rds:us-east-1:123456789012:cluster:test-docdb-cluster"), - State: stringPtr("Enabled"), + UUID: new("test-uuid-3"), + FunctionArn: new("arn:aws:lambda:us-east-1:123456789012:function:test-function-3"), + EventSourceArn: new("arn:aws:rds:us-east-1:123456789012:cluster:test-docdb-cluster"), + State: new("Enabled"), } item, err := adapter.ItemMapper("test-uuid-3", "123456789012.us-east-1", awsItem) diff --git a/aws-source/adapters/lambda-function_test.go b/aws-source/adapters/lambda-function_test.go index 8a444c81..df130bcc 100644 --- a/aws-source/adapters/lambda-function_test.go +++ b/aws-source/adapters/lambda-function_test.go @@ -12,18 +12,18 @@ import ( ) var testFuncConfig = &types.FunctionConfiguration{ - FunctionName: PtrString("aws-controltower-NotificationForwarder"), - FunctionArn: PtrString("arn:aws:lambda:eu-west-2:052392120703:function:aws-controltower-NotificationForwarder"), + FunctionName: new("aws-controltower-NotificationForwarder"), + FunctionArn: new("arn:aws:lambda:eu-west-2:052392120703:function:aws-controltower-NotificationForwarder"), Runtime: types.RuntimePython39, - Role: PtrString("arn:aws:iam::052392120703:role/aws-controltower-ForwardSnsNotificationRole"), // link - Handler: PtrString("index.lambda_handler"), + Role: new("arn:aws:iam::052392120703:role/aws-controltower-ForwardSnsNotificationRole"), // link + Handler: new("index.lambda_handler"), CodeSize: 473, - Description: PtrString("SNS message forwarding function for aggregating account notifications."), - Timeout: PtrInt32(60), - MemorySize: PtrInt32(128), - LastModified: PtrString("2022-12-13T15:22:48.157+0000"), - CodeSha256: PtrString("3zU7iYiZektHRaog6qOFvv34ggadB56rd/UMjnYms6A="), - Version: PtrString("$LATEST"), + Description: new("SNS message forwarding function for aggregating account notifications."), + Timeout: new(int32(60)), + MemorySize: new(int32(128)), + LastModified: new("2022-12-13T15:22:48.157+0000"), + CodeSha256: new("3zU7iYiZektHRaog6qOFvv34ggadB56rd/UMjnYms6A="), + Version: new("$LATEST"), Environment: &types.EnvironmentResponse{ Variables: map[string]string{ "sns_arn": "arn:aws:sns:eu-west-2:347195421325:aws-controltower-AggregateSecurityNotifications", @@ -32,7 +32,7 @@ var testFuncConfig = &types.FunctionConfiguration{ TracingConfig: &types.TracingConfigResponse{ Mode: types.TracingModePassThrough, }, - RevisionId: PtrString("b00dd2e6-eec3-48b0-abf1-f84406e00a3e"), + RevisionId: new("b00dd2e6-eec3-48b0-abf1-f84406e00a3e"), State: types.StateActive, LastUpdateStatus: types.LastUpdateStatusSuccessful, PackageType: types.PackageTypeZip, @@ -40,47 +40,47 @@ var testFuncConfig = &types.FunctionConfiguration{ types.ArchitectureX8664, }, EphemeralStorage: &types.EphemeralStorage{ - Size: PtrInt32(512), + Size: new(int32(512)), }, DeadLetterConfig: &types.DeadLetterConfig{ - TargetArn: PtrString("arn:aws:sns:us-east-2:444455556666:MyTopic"), // links + TargetArn: new("arn:aws:sns:us-east-2:444455556666:MyTopic"), // links }, FileSystemConfigs: []types.FileSystemConfig{ { - Arn: PtrString("arn:aws:service:region:account:type/id"), // links - LocalMountPath: PtrString("/config"), + Arn: new("arn:aws:service:region:account:type/id"), // links + LocalMountPath: new("/config"), }, }, ImageConfigResponse: &types.ImageConfigResponse{ Error: &types.ImageConfigError{ - ErrorCode: PtrString("500"), - Message: PtrString("borked"), + ErrorCode: new("500"), + Message: new("borked"), }, ImageConfig: &types.ImageConfig{ Command: []string{"echo", "foo"}, EntryPoint: []string{"/bin"}, - WorkingDirectory: PtrString("/"), + WorkingDirectory: new("/"), }, }, - KMSKeyArn: PtrString("arn:aws:service:region:account:type/id"), // link - LastUpdateStatusReason: PtrString("reason"), + KMSKeyArn: new("arn:aws:service:region:account:type/id"), // link + LastUpdateStatusReason: new("reason"), LastUpdateStatusReasonCode: types.LastUpdateStatusReasonCodeDisabledKMSKey, Layers: []types.Layer{ { - Arn: PtrString("arn:aws:service:region:account:layer:name:version"), // link + Arn: new("arn:aws:service:region:account:layer:name:version"), // link CodeSize: 128, - SigningJobArn: PtrString("arn:aws:service:region:account:type/id"), // link - SigningProfileVersionArn: PtrString("arn:aws:service:region:account:type/id"), // link + SigningJobArn: new("arn:aws:service:region:account:type/id"), // link + SigningProfileVersionArn: new("arn:aws:service:region:account:type/id"), // link }, }, - MasterArn: PtrString("arn:aws:service:region:account:type/id"), // link - SigningJobArn: PtrString("arn:aws:service:region:account:type/id"), // link - SigningProfileVersionArn: PtrString("arn:aws:service:region:account:type/id"), // link + MasterArn: new("arn:aws:service:region:account:type/id"), // link + SigningJobArn: new("arn:aws:service:region:account:type/id"), // link + SigningProfileVersionArn: new("arn:aws:service:region:account:type/id"), // link SnapStart: &types.SnapStartResponse{ ApplyOn: types.SnapStartApplyOnPublishedVersions, OptimizationStatus: types.SnapStartOptimizationStatusOn, }, - StateReason: PtrString("reason"), + StateReason: new("reason"), StateReasonCode: types.StateReasonCodeCreating, VpcConfig: &types.VpcConfigResponse{ SecurityGroupIds: []string{ @@ -89,15 +89,15 @@ var testFuncConfig = &types.FunctionConfiguration{ SubnetIds: []string{ "id", // link }, - VpcId: PtrString("id"), // link + VpcId: new("id"), // link }, } var testFuncCode = &types.FunctionCodeLocation{ - RepositoryType: PtrString("S3"), - Location: PtrString("https://awslambda-eu-west-2-tasks.s3.eu-west-2.amazonaws.com/snapshots/052392120703/aws-controltower-NotificationForwarder-bcea303b-7721-4cf0-b8db-7a0e6dca76dd?versionId=3Lk06tjGEoY451GYYupIohtTV96CkVKC&X-Amz-Security-Token=IQoJb3JpZ2l&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Etc=etcetcetc"), // link - ImageUri: PtrString("https://foo"), // link - ResolvedImageUri: PtrString("https://foo"), // link + RepositoryType: new("S3"), + Location: new("https://awslambda-eu-west-2-tasks.s3.eu-west-2.amazonaws.com/snapshots/052392120703/aws-controltower-NotificationForwarder-bcea303b-7721-4cf0-b8db-7a0e6dca76dd?versionId=3Lk06tjGEoY451GYYupIohtTV96CkVKC&X-Amz-Security-Token=IQoJb3JpZ2l&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Etc=etcetcetc"), // link + ImageUri: new("https://foo"), // link + ResolvedImageUri: new("https://foo"), // link } func (t *TestLambdaClient) GetFunction(ctx context.Context, params *lambda.GetFunctionInput, optFns ...func(*lambda.Options)) (*lambda.GetFunctionOutput, error) { @@ -118,16 +118,16 @@ func (t *TestLambdaClient) ListFunctionEventInvokeConfigs(context.Context, *lamb { DestinationConfig: &types.DestinationConfig{ OnFailure: &types.OnFailure{ - Destination: PtrString("arn:aws:events:region:account:event-bus/event-bus-name"), // link + Destination: new("arn:aws:events:region:account:event-bus/event-bus-name"), // link }, OnSuccess: &types.OnSuccess{ - Destination: PtrString("arn:aws:events:region:account:event-bus/event-bus-name"), // link + Destination: new("arn:aws:events:region:account:event-bus/event-bus-name"), // link }, }, - FunctionArn: PtrString("arn:aws:service:region:account:type/id"), - LastModified: PtrTime(time.Now()), - MaximumEventAgeInSeconds: PtrInt32(10), - MaximumRetryAttempts: PtrInt32(20), + FunctionArn: new("arn:aws:service:region:account:type/id"), + LastModified: new(time.Now()), + MaximumEventAgeInSeconds: new(int32(10)), + MaximumRetryAttempts: new(int32(20)), }, }, }, nil @@ -138,17 +138,17 @@ func (t *TestLambdaClient) ListFunctionUrlConfigs(context.Context, *lambda.ListF FunctionUrlConfigs: []types.FunctionUrlConfig{ { AuthType: types.FunctionUrlAuthTypeNone, - CreationTime: PtrString("recently"), - FunctionArn: PtrString("arn:aws:service:region:account:type/id"), - FunctionUrl: PtrString("https://bar"), // link - LastModifiedTime: PtrString("recently"), + CreationTime: new("recently"), + FunctionArn: new("arn:aws:service:region:account:type/id"), + FunctionUrl: new("https://bar"), // link + LastModifiedTime: new("recently"), Cors: &types.Cors{ - AllowCredentials: PtrBool(true), + AllowCredentials: new(true), AllowHeaders: []string{"X-Forwarded-For"}, AllowMethods: []string{"GET"}, AllowOrigins: []string{"https://bar"}, ExposeHeaders: []string{"X-Authentication"}, - MaxAge: PtrInt32(10), + MaxAge: new(int32(10)), }, }, }, diff --git a/aws-source/adapters/lambda-layer-version.go b/aws-source/adapters/lambda-layer-version.go index a62e6d71..aa505049 100644 --- a/aws-source/adapters/lambda-layer-version.go +++ b/aws-source/adapters/lambda-layer-version.go @@ -28,7 +28,7 @@ func layerVersionGetInputMapper(scope, query string) *lambda.GetLayerVersionInpu return &lambda.GetLayerVersionInput{ LayerName: &name, - VersionNumber: PtrInt64(int64(versionInt)), + VersionNumber: new(int64(versionInt)), } } @@ -107,7 +107,7 @@ func NewLambdaLayerVersionAdapter(client LambdaClient, accountID string, region GetFunc: layerVersionGetFunc, ListInput: &lambda.ListLayerVersionsInput{}, AdapterMetadata: layerVersionAdapterMetadata, - cache: cache, + cache: cache, ListFuncOutputMapper: func(output *lambda.ListLayerVersionsOutput, input *lambda.ListLayerVersionsInput) ([]*lambda.GetLayerVersionInput, error) { return []*lambda.GetLayerVersionInput{}, nil }, diff --git a/aws-source/adapters/lambda-layer-version_test.go b/aws-source/adapters/lambda-layer-version_test.go index 15d1f253..ccf6785b 100644 --- a/aws-source/adapters/lambda-layer-version_test.go +++ b/aws-source/adapters/lambda-layer-version_test.go @@ -62,17 +62,17 @@ func (t *TestLambdaClient) GetLayerVersion(ctx context.Context, params *lambda.G types.RuntimeDotnet6, }, Content: &types.LayerVersionContentOutput{ - CodeSha256: PtrString("sha"), + CodeSha256: new("sha"), CodeSize: 100, - Location: PtrString("somewhere"), - SigningJobArn: PtrString("arn:aws:service:region:account:type/id"), - SigningProfileVersionArn: PtrString("arn:aws:service:region:account:type/id"), + Location: new("somewhere"), + SigningJobArn: new("arn:aws:service:region:account:type/id"), + SigningProfileVersionArn: new("arn:aws:service:region:account:type/id"), }, - CreatedDate: PtrString("YYYY-MM-DDThh:mm:ss.sTZD"), - Description: PtrString("description"), - LayerArn: PtrString("arn:aws:service:region:account:type/id"), - LayerVersionArn: PtrString("arn:aws:service:region:account:type/id"), - LicenseInfo: PtrString("info"), + CreatedDate: new("YYYY-MM-DDThh:mm:ss.sTZD"), + Description: new("description"), + LayerArn: new("arn:aws:service:region:account:type/id"), + LayerVersionArn: new("arn:aws:service:region:account:type/id"), + LicenseInfo: new("info"), Version: *params.VersionNumber, }, nil } @@ -83,8 +83,8 @@ func (t *TestLambdaClient) ListLayerVersions(context.Context, *lambda.ListLayerV func TestLayerVersionGetFunc(t *testing.T) { item, err := layerVersionGetFunc(context.Background(), &TestLambdaClient{}, "foo", &lambda.GetLayerVersionInput{ - LayerName: PtrString("layer"), - VersionNumber: PtrInt64(999), + LayerName: new("layer"), + VersionNumber: new(int64(999)), }) if err != nil { diff --git a/aws-source/adapters/lambda-layer_test.go b/aws-source/adapters/lambda-layer_test.go index d402bdd1..0bf3d3db 100644 --- a/aws-source/adapters/lambda-layer_test.go +++ b/aws-source/adapters/lambda-layer_test.go @@ -19,14 +19,14 @@ func TestLayerItemMapper(t *testing.T) { CompatibleRuntimes: []types.Runtime{ types.RuntimeJava11, }, - CreatedDate: PtrString("2018-11-27T15:10:45.123+0000"), - Description: PtrString("description"), - LayerVersionArn: PtrString("arn:aws:service:region:account:type/id"), - LicenseInfo: PtrString("info"), + CreatedDate: new("2018-11-27T15:10:45.123+0000"), + Description: new("description"), + LayerVersionArn: new("arn:aws:service:region:account:type/id"), + LicenseInfo: new("info"), Version: 10, }, - LayerArn: PtrString("arn:aws:service:region:account:type/id"), - LayerName: PtrString("name"), + LayerArn: new("arn:aws:service:region:account:type/id"), + LayerName: new("name"), } item, err := layerItemMapper("", "foo", &layer) diff --git a/aws-source/adapters/lambda.go b/aws-source/adapters/lambda.go index 6d9ed511..30fe81a9 100644 --- a/aws-source/adapters/lambda.go +++ b/aws-source/adapters/lambda.go @@ -30,8 +30,8 @@ type PolicyDocument struct { // PolicyStatement defines a statement in a policy document. type PolicyStatement struct { Action string - Principal Principal `json:",omitempty"` - Condition Condition `json:",omitempty"` + Principal Principal + Condition Condition } type Principal struct { @@ -39,8 +39,8 @@ type Principal struct { } type Condition struct { - ArnLike ArnLikeCondition `json:",omitempty"` - StringEquals StringEqualsCondition `json:",omitempty"` + ArnLike ArnLikeCondition + StringEquals StringEqualsCondition } type StringEqualsCondition struct { diff --git a/aws-source/adapters/network-firewall-firewall-policy_test.go b/aws-source/adapters/network-firewall-firewall-policy_test.go index 092aab28..89e4a01a 100644 --- a/aws-source/adapters/network-firewall-firewall-policy_test.go +++ b/aws-source/adapters/network-firewall-firewall-policy_test.go @@ -13,23 +13,23 @@ func (c testNetworkFirewallClient) DescribeFirewallPolicy(ctx context.Context, p now := time.Now() return &networkfirewall.DescribeFirewallPolicyOutput{ FirewallPolicyResponse: &types.FirewallPolicyResponse{ - FirewallPolicyArn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), - FirewallPolicyId: PtrString("test"), - FirewallPolicyName: PtrString("test"), - ConsumedStatefulRuleCapacity: PtrInt32(1), - ConsumedStatelessRuleCapacity: PtrInt32(1), - Description: PtrString("test"), + FirewallPolicyArn: new("arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), + FirewallPolicyId: new("test"), + FirewallPolicyName: new("test"), + ConsumedStatefulRuleCapacity: new(int32(1)), + ConsumedStatelessRuleCapacity: new(int32(1)), + Description: new("test"), EncryptionConfiguration: &types.EncryptionConfiguration{ Type: types.EncryptionTypeAwsOwnedKmsKey, - KeyId: PtrString("arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"), // link (this can be an ARN or ID) + KeyId: new("arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"), // link (this can be an ARN or ID) }, FirewallPolicyStatus: types.ResourceStatusActive, // health LastModifiedTime: &now, - NumberOfAssociations: PtrInt32(1), + NumberOfAssociations: new(int32(1)), Tags: []types.Tag{ { - Key: PtrString("test"), - Value: PtrString("test"), + Key: new("test"), + Value: new("test"), }, }, }, @@ -50,11 +50,11 @@ func (c testNetworkFirewallClient) DescribeFirewallPolicy(ctx context.Context, p }, StatefulRuleGroupReferences: []types.StatefulRuleGroupReference{ { - ResourceArn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:stateful-rulegroup/aws-network-firewall-DefaultStatefulRuleGroup-1J3Z3W2ZQXV3"), // link + ResourceArn: new("arn:aws:network-firewall:us-east-1:123456789012:stateful-rulegroup/aws-network-firewall-DefaultStatefulRuleGroup-1J3Z3W2ZQXV3"), // link Override: &types.StatefulRuleGroupOverride{ Action: types.OverrideActionDropToAlert, }, - Priority: PtrInt32(1), + Priority: new(int32(1)), }, }, StatelessCustomActions: []types.CustomAction{ @@ -64,16 +64,16 @@ func (c testNetworkFirewallClient) DescribeFirewallPolicy(ctx context.Context, p Dimensions: []types.Dimension{}, }, }, - ActionName: PtrString("test"), + ActionName: new("test"), }, }, StatelessRuleGroupReferences: []types.StatelessRuleGroupReference{ { - Priority: PtrInt32(1), - ResourceArn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), // link + Priority: new(int32(1)), + ResourceArn: new("arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), // link }, }, - TLSInspectionConfigurationArn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:tls-inspection-configuration/aws-network-firewall-DefaultTlsInspectionConfiguration-1J3Z3W2ZQXV3"), // link + TLSInspectionConfigurationArn: new("arn:aws:network-firewall:us-east-1:123456789012:tls-inspection-configuration/aws-network-firewall-DefaultTlsInspectionConfiguration-1J3Z3W2ZQXV3"), // link }, }, nil } @@ -82,7 +82,7 @@ func (c testNetworkFirewallClient) ListFirewallPolicies(context.Context, *networ return &networkfirewall.ListFirewallPoliciesOutput{ FirewallPolicies: []types.FirewallPolicyMetadata{ { - Arn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), + Arn: new("arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), }, }, }, nil diff --git a/aws-source/adapters/network-firewall-firewall_test.go b/aws-source/adapters/network-firewall-firewall_test.go index 1092e319..8506ecfa 100644 --- a/aws-source/adapters/network-firewall-firewall_test.go +++ b/aws-source/adapters/network-firewall-firewall_test.go @@ -13,29 +13,29 @@ import ( func (c testNetworkFirewallClient) DescribeFirewall(ctx context.Context, params *networkfirewall.DescribeFirewallInput, optFns ...func(*networkfirewall.Options)) (*networkfirewall.DescribeFirewallOutput, error) { return &networkfirewall.DescribeFirewallOutput{ Firewall: &types.Firewall{ - FirewallId: PtrString("test"), - FirewallPolicyArn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), // link + FirewallId: new("test"), + FirewallPolicyArn: new("arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), // link SubnetMappings: []types.SubnetMapping{ { - SubnetId: PtrString("subnet-12345678901234567"), // link + SubnetId: new("subnet-12345678901234567"), // link IPAddressType: types.IPAddressTypeIpv4, }, }, - VpcId: PtrString("vpc-12345678901234567"), // link + VpcId: new("vpc-12345678901234567"), // link DeleteProtection: false, - Description: PtrString("test"), + Description: new("test"), EncryptionConfiguration: &types.EncryptionConfiguration{ Type: types.EncryptionTypeAwsOwnedKmsKey, - KeyId: PtrString("arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"), // link (this can be an ARN or ID) + KeyId: new("arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"), // link (this can be an ARN or ID) }, - FirewallArn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:firewall/aws-network-firewall-DefaultFirewall-1J3Z3W2ZQXV3"), - FirewallName: PtrString("test"), + FirewallArn: new("arn:aws:network-firewall:us-east-1:123456789012:firewall/aws-network-firewall-DefaultFirewall-1J3Z3W2ZQXV3"), + FirewallName: new("test"), FirewallPolicyChangeProtection: false, SubnetChangeProtection: false, Tags: []types.Tag{ { - Key: PtrString("test"), - Value: PtrString("test"), + Key: new("test"), + Value: new("test"), }, }, }, @@ -44,22 +44,22 @@ func (c testNetworkFirewallClient) DescribeFirewall(ctx context.Context, params Status: types.FirewallStatusValueDeleting, CapacityUsageSummary: &types.CapacityUsageSummary{ CIDRs: &types.CIDRSummary{ - AvailableCIDRCount: PtrInt32(1), + AvailableCIDRCount: new(int32(1)), IPSetReferences: map[string]types.IPSetMetadata{ "test": { - ResolvedCIDRCount: PtrInt32(1), + ResolvedCIDRCount: new(int32(1)), }, }, - UtilizedCIDRCount: PtrInt32(1), + UtilizedCIDRCount: new(int32(1)), }, }, SyncStates: map[string]types.SyncState{ "test": { Attachment: &types.Attachment{ - EndpointId: PtrString("test"), + EndpointId: new("test"), Status: types.AttachmentStatusCreating, - StatusMessage: PtrString("test"), - SubnetId: PtrString("test"), // link, + StatusMessage: new("test"), + SubnetId: new("test"), // link, }, }, }, @@ -69,7 +69,7 @@ func (c testNetworkFirewallClient) DescribeFirewall(ctx context.Context, params func (c testNetworkFirewallClient) DescribeLoggingConfiguration(ctx context.Context, params *networkfirewall.DescribeLoggingConfigurationInput, optFns ...func(*networkfirewall.Options)) (*networkfirewall.DescribeLoggingConfigurationOutput, error) { return &networkfirewall.DescribeLoggingConfigurationOutput{ - FirewallArn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:firewall/aws-network-firewall-DefaultFirewall-1J3Z3W2ZQXV3"), + FirewallArn: new("arn:aws:network-firewall:us-east-1:123456789012:firewall/aws-network-firewall-DefaultFirewall-1J3Z3W2ZQXV3"), LoggingConfiguration: &types.LoggingConfiguration{ LogDestinationConfigs: []types.LogDestinationConfig{ { @@ -101,7 +101,7 @@ func (c testNetworkFirewallClient) DescribeLoggingConfiguration(ctx context.Cont func (c testNetworkFirewallClient) DescribeResourcePolicy(ctx context.Context, params *networkfirewall.DescribeResourcePolicyInput, optFns ...func(*networkfirewall.Options)) (*networkfirewall.DescribeResourcePolicyOutput, error) { return &networkfirewall.DescribeResourcePolicyOutput{ - Policy: PtrString("test"), // link + Policy: new("test"), // link }, nil } @@ -109,7 +109,7 @@ func (c testNetworkFirewallClient) ListFirewalls(context.Context, *networkfirewa return &networkfirewall.ListFirewallsOutput{ Firewalls: []types.FirewallMetadata{ { - FirewallArn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:firewall/aws-network-firewall-DefaultFirewall-1J3Z3W2ZQXV3"), + FirewallArn: new("arn:aws:network-firewall:us-east-1:123456789012:firewall/aws-network-firewall-DefaultFirewall-1J3Z3W2ZQXV3"), }, }, }, nil diff --git a/aws-source/adapters/network-firewall-rule-group_test.go b/aws-source/adapters/network-firewall-rule-group_test.go index c5dd65a6..afd83434 100644 --- a/aws-source/adapters/network-firewall-rule-group_test.go +++ b/aws-source/adapters/network-firewall-rule-group_test.go @@ -14,37 +14,37 @@ func (c testNetworkFirewallClient) DescribeRuleGroup(ctx context.Context, params return &networkfirewall.DescribeRuleGroupOutput{ RuleGroupResponse: &types.RuleGroupResponse{ - RuleGroupArn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), - RuleGroupId: PtrString("test"), - RuleGroupName: PtrString("test"), + RuleGroupArn: new("arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), + RuleGroupId: new("test"), + RuleGroupName: new("test"), AnalysisResults: []types.AnalysisResult{ { - AnalysisDetail: PtrString("test"), + AnalysisDetail: new("test"), IdentifiedRuleIds: []string{ "test", }, IdentifiedType: types.IdentifiedTypeStatelessRuleContainsTcpFlags, }, }, - Capacity: PtrInt32(1), - ConsumedCapacity: PtrInt32(1), - Description: PtrString("test"), + Capacity: new(int32(1)), + ConsumedCapacity: new(int32(1)), + Description: new("test"), EncryptionConfiguration: &types.EncryptionConfiguration{ Type: types.EncryptionTypeAwsOwnedKmsKey, - KeyId: PtrString("arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"), // link (this can be an ARN or ID) + KeyId: new("arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"), // link (this can be an ARN or ID) }, LastModifiedTime: &now, - NumberOfAssociations: PtrInt32(1), - RuleGroupStatus: types.ResourceStatusActive, // health - SnsTopic: PtrString("arn:aws:sns:us-east-1:123456789012:aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), // link + NumberOfAssociations: new(int32(1)), + RuleGroupStatus: types.ResourceStatusActive, // health + SnsTopic: new("arn:aws:sns:us-east-1:123456789012:aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), // link SourceMetadata: &types.SourceMetadata{ - SourceArn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:firewall/aws-network-firewall-DefaultFirewall-1J3Z3W2ZQXV3"), // link - SourceUpdateToken: PtrString("test"), + SourceArn: new("arn:aws:network-firewall:us-east-1:123456789012:firewall/aws-network-firewall-DefaultFirewall-1J3Z3W2ZQXV3"), // link + SourceUpdateToken: new("test"), }, Tags: []types.Tag{ { - Key: PtrString("test"), - Value: PtrString("test"), + Key: new("test"), + Value: new("test"), }, }, Type: types.RuleGroupTypeStateless, @@ -60,24 +60,24 @@ func (c testNetworkFirewallClient) DescribeRuleGroup(ctx context.Context, params "foo.bar.com", // link }, }, - RulesString: PtrString("test"), + RulesString: new("test"), StatefulRules: []types.StatefulRule{ { Action: types.StatefulActionAlert, Header: &types.Header{ - Destination: PtrString("1.1.1.1"), - DestinationPort: PtrString("8080"), + Destination: new("1.1.1.1"), + DestinationPort: new("8080"), Direction: types.StatefulRuleDirectionForward, Protocol: types.StatefulRuleProtocolDcerpc, - Source: PtrString("test"), - SourcePort: PtrString("8080"), + Source: new("test"), + SourcePort: new("8080"), }, }, }, StatelessRulesAndCustomActions: &types.StatelessRulesAndCustomActions{ StatelessRules: []types.StatelessRule{ { - Priority: PtrInt32(1), + Priority: new(int32(1)), RuleDefinition: &types.RuleDefinition{ Actions: []string{}, MatchAttributes: &types.MatchAttributes{ @@ -89,7 +89,7 @@ func (c testNetworkFirewallClient) DescribeRuleGroup(ctx context.Context, params }, Destinations: []types.Address{ { - AddressDefinition: PtrString("1.1.1.1/1"), + AddressDefinition: new("1.1.1.1/1"), }, }, Protocols: []int32{1}, @@ -120,12 +120,12 @@ func (c testNetworkFirewallClient) DescribeRuleGroup(ctx context.Context, params PublishMetricAction: &types.PublishMetricAction{ Dimensions: []types.Dimension{ { - Value: PtrString("test"), + Value: new("test"), }, }, }, }, - ActionName: PtrString("test"), + ActionName: new("test"), }, }, }, @@ -138,7 +138,7 @@ func (c testNetworkFirewallClient) ListRuleGroups(ctx context.Context, params *n return &networkfirewall.ListRuleGroupsOutput{ RuleGroups: []types.RuleGroupMetadata{ { - Arn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), + Arn: new("arn:aws:network-firewall:us-east-1:123456789012:stateless-rulegroup/aws-network-firewall-DefaultStatelessRuleGroup-1J3Z3W2ZQXV3"), }, }, }, nil 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 1ff37cdb..4b2333dd 100644 --- a/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go +++ b/aws-source/adapters/network-firewall-tls-inspection-configuration_test.go @@ -13,42 +13,42 @@ func (c testNetworkFirewallClient) DescribeTLSInspectionConfiguration(ctx contex now := time.Now() return &networkfirewall.DescribeTLSInspectionConfigurationOutput{ TLSInspectionConfigurationResponse: &types.TLSInspectionConfigurationResponse{ - TLSInspectionConfigurationArn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:tls-inspection-configuration/aws-network-firewall-DefaultTLSInspectionConfiguration-1J3Z3W2ZQXV3"), - TLSInspectionConfigurationId: PtrString("test"), - TLSInspectionConfigurationName: PtrString("test"), + TLSInspectionConfigurationArn: new("arn:aws:network-firewall:us-east-1:123456789012:tls-inspection-configuration/aws-network-firewall-DefaultTLSInspectionConfiguration-1J3Z3W2ZQXV3"), + TLSInspectionConfigurationId: new("test"), + TLSInspectionConfigurationName: new("test"), CertificateAuthority: &types.TlsCertificateData{ - CertificateArn: PtrString("arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"), // link - CertificateSerial: PtrString("test"), - Status: PtrString("OK"), - StatusMessage: PtrString("test"), + CertificateArn: new("arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"), // link + CertificateSerial: new("test"), + Status: new("OK"), + StatusMessage: new("test"), }, Certificates: []types.TlsCertificateData{ { - CertificateArn: PtrString("arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"), // link - CertificateSerial: PtrString("test"), - Status: PtrString("OK"), - StatusMessage: PtrString("test"), + CertificateArn: new("arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"), // link + CertificateSerial: new("test"), + Status: new("OK"), + StatusMessage: new("test"), }, }, - Description: PtrString("test"), + Description: new("test"), EncryptionConfiguration: &types.EncryptionConfiguration{ Type: types.EncryptionTypeAwsOwnedKmsKey, - KeyId: PtrString("arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"), // link (this can be an ARN or ID) + KeyId: new("arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"), // link (this can be an ARN or ID) }, LastModifiedTime: &now, - NumberOfAssociations: PtrInt32(1), + NumberOfAssociations: new(int32(1)), TLSInspectionConfigurationStatus: types.ResourceStatusActive, // health Tags: []types.Tag{ { - Key: PtrString("test"), - Value: PtrString("test"), + Key: new("test"), + Value: new("test"), }, }, }, TLSInspectionConfiguration: &types.TLSInspectionConfiguration{ ServerCertificateConfigurations: []types.ServerCertificateConfiguration{ { - CertificateAuthorityArn: PtrString("arn:aws:acm:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012"), // link + CertificateAuthorityArn: new("arn:aws:acm:us-east-1:123456789012:certificate-authority/12345678-1234-1234-1234-123456789012"), // link CheckCertificateRevocationStatus: &types.CheckCertificateRevocationStatusActions{ RevokedStatusAction: types.RevocationCheckActionPass, UnknownStatusAction: types.RevocationCheckActionPass, @@ -63,7 +63,7 @@ func (c testNetworkFirewallClient) DescribeTLSInspectionConfiguration(ctx contex }, Destinations: []types.Address{ { - AddressDefinition: PtrString("test"), + AddressDefinition: new("test"), }, }, Protocols: []int32{1}, @@ -75,14 +75,14 @@ func (c testNetworkFirewallClient) DescribeTLSInspectionConfiguration(ctx contex }, Sources: []types.Address{ { - AddressDefinition: PtrString("test"), + AddressDefinition: new("test"), }, }, }, }, ServerCertificates: []types.ServerCertificate{ { - ResourceArn: PtrString("arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"), // link + ResourceArn: new("arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"), // link }, }, }, @@ -95,7 +95,7 @@ func (c testNetworkFirewallClient) ListTLSInspectionConfigurations(ctx context.C return &networkfirewall.ListTLSInspectionConfigurationsOutput{ TLSInspectionConfigurations: []types.TLSInspectionConfigurationMetadata{ { - Arn: PtrString("arn:aws:network-firewall:us-east-1:123456789012:tls-inspection-configuration/aws-network-firewall-DefaultTLSInspectionConfiguration-1J3Z3W2ZQXV3"), + Arn: new("arn:aws:network-firewall:us-east-1:123456789012:tls-inspection-configuration/aws-network-firewall-DefaultTLSInspectionConfiguration-1J3Z3W2ZQXV3"), }, }, }, nil diff --git a/aws-source/adapters/networkmanager-connect-attachment_test.go b/aws-source/adapters/networkmanager-connect-attachment_test.go index 7e9479fe..b136dfc5 100644 --- a/aws-source/adapters/networkmanager-connect-attachment_test.go +++ b/aws-source/adapters/networkmanager-connect-attachment_test.go @@ -12,9 +12,9 @@ func TestConnectAttachmentItemMapper(t *testing.T) { scope := "123456789012.eu-west-2" item, err := connectAttachmentItemMapper("", scope, &types.ConnectAttachment{ Attachment: &types.Attachment{ - AttachmentId: PtrString("att-1"), - CoreNetworkId: PtrString("cn-1"), - CoreNetworkArn: PtrString("arn:aws:networkmanager:eu-west-2:123456789012:core-network/cn-1"), + AttachmentId: new("att-1"), + CoreNetworkId: new("cn-1"), + CoreNetworkArn: new("arn:aws:networkmanager:eu-west-2:123456789012:core-network/cn-1"), }, }) if err != nil { diff --git a/aws-source/adapters/networkmanager-connect-peer-association_test.go b/aws-source/adapters/networkmanager-connect-peer-association_test.go index 501a4c56..dfa333e3 100644 --- a/aws-source/adapters/networkmanager-connect-peer-association_test.go +++ b/aws-source/adapters/networkmanager-connect-peer-association_test.go @@ -14,10 +14,10 @@ func TestConnectPeerAssociationsOutputMapper(t *testing.T) { output := networkmanager.GetConnectPeerAssociationsOutput{ ConnectPeerAssociations: []types.ConnectPeerAssociation{ { - ConnectPeerId: PtrString("cp-1"), - DeviceId: PtrString("dvc-1"), - GlobalNetworkId: PtrString("default"), - LinkId: PtrString("link-1"), + ConnectPeerId: new("cp-1"), + DeviceId: new("dvc-1"), + GlobalNetworkId: new("default"), + LinkId: new("link-1"), }, }, } diff --git a/aws-source/adapters/networkmanager-connect-peer_test.go b/aws-source/adapters/networkmanager-connect-peer_test.go index c653214d..7f11e080 100644 --- a/aws-source/adapters/networkmanager-connect-peer_test.go +++ b/aws-source/adapters/networkmanager-connect-peer_test.go @@ -16,21 +16,21 @@ func (n NetworkManagerTestClient) GetConnectPeer(ctx context.Context, params *ne Configuration: &types.ConnectPeerConfiguration{ BgpConfigurations: []types.ConnectPeerBgpConfiguration{ { - CoreNetworkAddress: PtrString("1.4.2.4"), // link - CoreNetworkAsn: PtrInt64(64512), // link - PeerAddress: PtrString("123.123.123.123"), // link - PeerAsn: PtrInt64(64513), // link + CoreNetworkAddress: new("1.4.2.4"), // link + CoreNetworkAsn: new(int64(64512)), // link + PeerAddress: new("123.123.123.123"), // link + PeerAsn: new(int64(64513)), // link }, }, - CoreNetworkAddress: PtrString("1.1.1.3"), // link - PeerAddress: PtrString("1.1.1.45"), // link + CoreNetworkAddress: new("1.1.1.3"), // link + PeerAddress: new("1.1.1.45"), // link }, - ConnectAttachmentId: PtrString("ca-1"), // link - ConnectPeerId: PtrString("cp-1"), - CoreNetworkId: PtrString("cn-1"), // link - EdgeLocation: PtrString("us-west-2"), + ConnectAttachmentId: new("ca-1"), // link + ConnectPeerId: new("cp-1"), + CoreNetworkId: new("cn-1"), // link + EdgeLocation: new("us-west-2"), State: types.ConnectPeerStateAvailable, - SubnetArn: PtrString("arn:aws:ec2:us-west-2:123456789012:subnet/subnet-1"), // link + SubnetArn: new("arn:aws:ec2:us-west-2:123456789012:subnet/subnet-1"), // link }, }, nil } diff --git a/aws-source/adapters/networkmanager-connection_test.go b/aws-source/adapters/networkmanager-connection_test.go index a2cf5f1d..957ff9fb 100644 --- a/aws-source/adapters/networkmanager-connection_test.go +++ b/aws-source/adapters/networkmanager-connection_test.go @@ -15,12 +15,12 @@ func TestConnectionOutputMapper(t *testing.T) { output := networkmanager.GetConnectionsOutput{ Connections: []types.Connection{ { - GlobalNetworkId: PtrString("default"), - ConnectionId: PtrString("conn-1"), - DeviceId: PtrString("dvc-1"), - ConnectedDeviceId: PtrString("dvc-2"), - LinkId: PtrString("link-1"), - ConnectedLinkId: PtrString("link-2"), + GlobalNetworkId: new("default"), + ConnectionId: new("conn-1"), + DeviceId: new("dvc-1"), + ConnectedDeviceId: new("dvc-2"), + LinkId: new("link-1"), + ConnectedLinkId: new("link-2"), }, }, } @@ -103,7 +103,7 @@ func TestConnectionInputMapperSearch(t *testing.T) { name: "Valid networkmanager-connection ARN", query: "arn:aws:networkmanager::123456789012:device/global-network-0d47f6t230mz46dy4/connection-07f6fd08867abc123", expectedInput: &networkmanager.GetConnectionsInput{ - GlobalNetworkId: PtrString("global-network-0d47f6t230mz46dy4"), + GlobalNetworkId: new("global-network-0d47f6t230mz46dy4"), ConnectionIds: []string{"connection-07f6fd08867abc123"}, }, expectError: false, @@ -112,8 +112,8 @@ func TestConnectionInputMapperSearch(t *testing.T) { name: "Valid networkmanager-device ARN", query: "arn:aws:networkmanager::123456789012:device/global-network-01231231231231231/device-07f6fd08867abc123", expectedInput: &networkmanager.GetConnectionsInput{ - GlobalNetworkId: PtrString("global-network-01231231231231231"), - DeviceId: PtrString("device-07f6fd08867abc123"), + GlobalNetworkId: new("global-network-01231231231231231"), + DeviceId: new("device-07f6fd08867abc123"), }, expectError: false, }, @@ -121,7 +121,7 @@ func TestConnectionInputMapperSearch(t *testing.T) { name: "Global Network ID only", query: "global-network-123456789", expectedInput: &networkmanager.GetConnectionsInput{ - GlobalNetworkId: PtrString("global-network-123456789"), + GlobalNetworkId: new("global-network-123456789"), }, expectError: false, }, @@ -129,8 +129,8 @@ func TestConnectionInputMapperSearch(t *testing.T) { name: "Global Network ID and Device ID", query: "global-network-123456789|device-987654321", expectedInput: &networkmanager.GetConnectionsInput{ - GlobalNetworkId: PtrString("global-network-123456789"), - DeviceId: PtrString("device-987654321"), + GlobalNetworkId: new("global-network-123456789"), + DeviceId: new("device-987654321"), }, expectError: false, }, diff --git a/aws-source/adapters/networkmanager-core-network-policy_test.go b/aws-source/adapters/networkmanager-core-network-policy_test.go index b0bd39c5..0307808b 100644 --- a/aws-source/adapters/networkmanager-core-network-policy_test.go +++ b/aws-source/adapters/networkmanager-core-network-policy_test.go @@ -11,8 +11,8 @@ func TestCoreNetworkPolicyItemMapper(t *testing.T) { scope := "123456789012.eu-west-2" item, err := coreNetworkPolicyItemMapper("", scope, &types.CoreNetworkPolicy{ - CoreNetworkId: PtrString("cn-1"), - PolicyVersionId: PtrInt32(1), + CoreNetworkId: new("cn-1"), + PolicyVersionId: new(int32(1)), }) if err != nil { t.Error(err) diff --git a/aws-source/adapters/networkmanager-core-network_test.go b/aws-source/adapters/networkmanager-core-network_test.go index 9283b49d..aa43722a 100644 --- a/aws-source/adapters/networkmanager-core-network_test.go +++ b/aws-source/adapters/networkmanager-core-network_test.go @@ -13,21 +13,21 @@ import ( func (n NetworkManagerTestClient) GetCoreNetwork(ctx context.Context, params *networkmanager.GetCoreNetworkInput, optFns ...func(*networkmanager.Options)) (*networkmanager.GetCoreNetworkOutput, error) { return &networkmanager.GetCoreNetworkOutput{ CoreNetwork: &types.CoreNetwork{ - CoreNetworkArn: PtrString("arn:aws:networkmanager:us-west-2:123456789012:core-network/cn-1"), - CoreNetworkId: PtrString("cn-1"), - GlobalNetworkId: PtrString("default"), - Description: PtrString("core network description"), + CoreNetworkArn: new("arn:aws:networkmanager:us-west-2:123456789012:core-network/cn-1"), + CoreNetworkId: new("cn-1"), + GlobalNetworkId: new("default"), + Description: new("core network description"), State: types.CoreNetworkStateAvailable, Edges: []types.CoreNetworkEdge{ { - Asn: PtrInt64(64512), // link - EdgeLocation: PtrString("us-west-2"), + Asn: new(int64(64512)), // link + EdgeLocation: new("us-west-2"), }, }, Segments: []types.CoreNetworkSegment{ { EdgeLocations: []string{"us-west-2"}, - Name: PtrString("segment-1"), + Name: new("segment-1"), }, }, }, diff --git a/aws-source/adapters/networkmanager-device_test.go b/aws-source/adapters/networkmanager-device_test.go index 190278c3..7c800872 100644 --- a/aws-source/adapters/networkmanager-device_test.go +++ b/aws-source/adapters/networkmanager-device_test.go @@ -15,10 +15,10 @@ func TestDeviceOutputMapper(t *testing.T) { output := networkmanager.GetDevicesOutput{ Devices: []types.Device{ { - DeviceId: PtrString("dvc-1"), - GlobalNetworkId: PtrString("default"), - SiteId: PtrString("site-1"), - DeviceArn: PtrString("arn:aws:networkmanager:us-west-2:123456789012:device/dvc-1"), + DeviceId: new("dvc-1"), + GlobalNetworkId: new("default"), + SiteId: new("site-1"), + DeviceArn: new("arn:aws:networkmanager:us-west-2:123456789012:device/dvc-1"), }, }, } @@ -101,7 +101,7 @@ func TestDeviceInputMapperSearch(t *testing.T) { name: "Valid networkmanager-device ARN", query: "arn:aws:networkmanager::123456789012:device/global-network-01231231231231231/device-07f6fd08867abc123", expectedInput: &networkmanager.GetDevicesInput{ - GlobalNetworkId: PtrString("global-network-01231231231231231"), + GlobalNetworkId: new("global-network-01231231231231231"), DeviceIds: []string{"device-07f6fd08867abc123"}, }, expectError: false, @@ -110,7 +110,7 @@ func TestDeviceInputMapperSearch(t *testing.T) { name: "Global Network ID only", query: "global-network-123456789", expectedInput: &networkmanager.GetDevicesInput{ - GlobalNetworkId: PtrString("global-network-123456789"), + GlobalNetworkId: new("global-network-123456789"), }, expectError: false, }, @@ -118,8 +118,8 @@ func TestDeviceInputMapperSearch(t *testing.T) { name: "Global Network ID and Site ID", query: "global-network-123456789|site-987654321", expectedInput: &networkmanager.GetDevicesInput{ - GlobalNetworkId: PtrString("global-network-123456789"), - SiteId: PtrString("site-987654321"), + GlobalNetworkId: new("global-network-123456789"), + SiteId: new("site-987654321"), }, expectError: false, }, diff --git a/aws-source/adapters/networkmanager-global-network_test.go b/aws-source/adapters/networkmanager-global-network_test.go index 8407c7b7..795e4f9b 100644 --- a/aws-source/adapters/networkmanager-global-network_test.go +++ b/aws-source/adapters/networkmanager-global-network_test.go @@ -14,8 +14,8 @@ func TestGlobalNetworkOutputMapper(t *testing.T) { output := networkmanager.DescribeGlobalNetworksOutput{ GlobalNetworks: []types.GlobalNetwork{ { - GlobalNetworkArn: PtrString("arn:aws:networkmanager:eu-west-2:052392120703:networkmanager/global-network/default"), - GlobalNetworkId: PtrString("default"), + GlobalNetworkArn: new("arn:aws:networkmanager:eu-west-2:052392120703:networkmanager/global-network/default"), + GlobalNetworkId: new("default"), }, }, } diff --git a/aws-source/adapters/networkmanager-link-association_test.go b/aws-source/adapters/networkmanager-link-association_test.go index cb74c060..7f9c21d6 100644 --- a/aws-source/adapters/networkmanager-link-association_test.go +++ b/aws-source/adapters/networkmanager-link-association_test.go @@ -14,9 +14,9 @@ func TestLinkAssociationOutputMapper(t *testing.T) { output := networkmanager.GetLinkAssociationsOutput{ LinkAssociations: []types.LinkAssociation{ { - LinkId: PtrString("link-1"), - GlobalNetworkId: PtrString("default"), - DeviceId: PtrString("dvc-1"), + LinkId: new("link-1"), + GlobalNetworkId: new("default"), + DeviceId: new("dvc-1"), }, }, } diff --git a/aws-source/adapters/networkmanager-link_test.go b/aws-source/adapters/networkmanager-link_test.go index 363d6838..c9b21afd 100644 --- a/aws-source/adapters/networkmanager-link_test.go +++ b/aws-source/adapters/networkmanager-link_test.go @@ -15,10 +15,10 @@ func TestLinkOutputMapper(t *testing.T) { output := networkmanager.GetLinksOutput{ Links: []types.Link{ { - LinkId: PtrString("link-1"), - GlobalNetworkId: PtrString("default"), - SiteId: PtrString("site-1"), - LinkArn: PtrString("arn:aws:networkmanager:us-west-2:123456789012:link/link-1"), + LinkId: new("link-1"), + GlobalNetworkId: new("default"), + SiteId: new("site-1"), + LinkArn: new("arn:aws:networkmanager:us-west-2:123456789012:link/link-1"), }, }, } @@ -95,7 +95,7 @@ func TestLinkInputMapperSearch(t *testing.T) { name: "Valid networkmanager-link ARN", query: "arn:aws:networkmanager::123456789012:link/global-network-01231231231231231/link-11112222aaaabbbb1", expectedInput: &networkmanager.GetLinksInput{ - GlobalNetworkId: PtrString("global-network-01231231231231231"), + GlobalNetworkId: new("global-network-01231231231231231"), LinkIds: []string{"link-11112222aaaabbbb1"}, }, expectError: false, @@ -104,7 +104,7 @@ func TestLinkInputMapperSearch(t *testing.T) { name: "Global Network ID only", query: "global-network-123456789", expectedInput: &networkmanager.GetLinksInput{ - GlobalNetworkId: PtrString("global-network-123456789"), + GlobalNetworkId: new("global-network-123456789"), }, expectError: false, }, @@ -112,8 +112,8 @@ func TestLinkInputMapperSearch(t *testing.T) { name: "Global Network ID and Site ID", query: "global-network-123456789|site-987654321", expectedInput: &networkmanager.GetLinksInput{ - GlobalNetworkId: PtrString("global-network-123456789"), - SiteId: PtrString("site-987654321"), + GlobalNetworkId: new("global-network-123456789"), + SiteId: new("site-987654321"), }, expectError: false, }, diff --git a/aws-source/adapters/networkmanager-network-resource-relationship.go b/aws-source/adapters/networkmanager-network-resource-relationship.go index cd6019cb..65e41a4d 100644 --- a/aws-source/adapters/networkmanager-network-resource-relationship.go +++ b/aws-source/adapters/networkmanager-network-resource-relationship.go @@ -43,7 +43,7 @@ func networkResourceRelationshipOutputMapper(_ context.Context, _ *networkmanage hasher.Write([]byte(toArn.String())) sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) - attrs, err := sdp.ToAttributes(map[string]interface{}{ + attrs, err := sdp.ToAttributes(map[string]any{ "Hash": sha, "From": fromArn.String(), "To": toArn.String(), @@ -197,7 +197,7 @@ func NewNetworkManagerNetworkResourceRelationshipsAdapter(client *networkmanager Region: region, ItemType: "networkmanager-network-resource-relationship", AdapterMetadata: networkResourceRelationshipAdapterMetadata, - cache: cache, + cache: cache, OutputMapper: networkResourceRelationshipOutputMapper, DescribeFunc: func(ctx context.Context, client *networkmanager.Client, input *networkmanager.GetNetworkResourceRelationshipsInput) (*networkmanager.GetNetworkResourceRelationshipsOutput, error) { return client.GetNetworkResourceRelationships(ctx, input) diff --git a/aws-source/adapters/networkmanager-network-resource-relationship_test.go b/aws-source/adapters/networkmanager-network-resource-relationship_test.go index 78dd910c..f3efdc03 100644 --- a/aws-source/adapters/networkmanager-network-resource-relationship_test.go +++ b/aws-source/adapters/networkmanager-network-resource-relationship_test.go @@ -21,72 +21,72 @@ func TestNetworkResourceRelationshipOutputMapper(t *testing.T) { { name: "ok, one entity", input: networkmanager.GetNetworkResourceRelationshipsInput{ - GlobalNetworkId: PtrString("default"), + GlobalNetworkId: new("default"), }, output: networkmanager.GetNetworkResourceRelationshipsOutput{ Relationships: []types.Relationship{ // connection, device { - From: PtrString("arn:aws:networkmanager:us-west-2:123456789012:connection/conn-1"), - To: PtrString("arn:aws:networkmanager:us-west-2:123456789012:device/d-1"), + From: new("arn:aws:networkmanager:us-west-2:123456789012:connection/conn-1"), + To: new("arn:aws:networkmanager:us-west-2:123456789012:device/d-1"), }, { - To: PtrString("arn:aws:networkmanager:us-west-2:123456789012:connection/conn-1"), - From: PtrString("arn:aws:networkmanager:us-west-2:123456789012:device/d-1"), + To: new("arn:aws:networkmanager:us-west-2:123456789012:connection/conn-1"), + From: new("arn:aws:networkmanager:us-west-2:123456789012:device/d-1"), }, // link, site { - From: PtrString("arn:aws:networkmanager:us-west-2:123456789012:link/link-1"), - To: PtrString("arn:aws:networkmanager:us-west-2:123456789012:site/site-1"), + From: new("arn:aws:networkmanager:us-west-2:123456789012:link/link-1"), + To: new("arn:aws:networkmanager:us-west-2:123456789012:site/site-1"), }, { - To: PtrString("arn:aws:networkmanager:us-west-2:123456789012:link/link-1"), - From: PtrString("arn:aws:networkmanager:us-west-2:123456789012:site/site-1"), + To: new("arn:aws:networkmanager:us-west-2:123456789012:link/link-1"), + From: new("arn:aws:networkmanager:us-west-2:123456789012:site/site-1"), }, // directconnect-connection, directconnect-direct-connect-gateway { - From: PtrString("arn:aws:directconnect:us-west-2:123456789012:connection/dxconn-1"), - To: PtrString("arn:aws:directconnect:us-west-2:123456789012:direct-connect-gateway/gw-1"), + From: new("arn:aws:directconnect:us-west-2:123456789012:connection/dxconn-1"), + To: new("arn:aws:directconnect:us-west-2:123456789012:direct-connect-gateway/gw-1"), }, { - To: PtrString("arn:aws:directconnect:us-west-2:123456789012:connection/dxconn-1"), - From: PtrString("arn:aws:directconnect:us-west-2:123456789012:direct-connect-gateway/gw-1"), + To: new("arn:aws:directconnect:us-west-2:123456789012:connection/dxconn-1"), + From: new("arn:aws:directconnect:us-west-2:123456789012:direct-connect-gateway/gw-1"), }, // directconnect-virtual-interface, ec2-customer-gateway { - From: PtrString("arn:aws:directconnect:us-west-2:123456789012:virtual-interface/vif-1"), - To: PtrString("arn:aws:ec2:us-west-2:123456789012:customer-gateway/gw-1"), + From: new("arn:aws:directconnect:us-west-2:123456789012:virtual-interface/vif-1"), + To: new("arn:aws:ec2:us-west-2:123456789012:customer-gateway/gw-1"), }, { - To: PtrString("arn:aws:directconnect:us-west-2:123456789012:virtual-interface/vif-1"), - From: PtrString("arn:aws:ec2:us-west-2:123456789012:customer-gateway/gw-1"), + To: new("arn:aws:directconnect:us-west-2:123456789012:virtual-interface/vif-1"), + From: new("arn:aws:ec2:us-west-2:123456789012:customer-gateway/gw-1"), }, // ec2-transit-gateway, ec2-transit-gateway-attachment { - From: PtrString("arn:aws:ec2:us-east-2:986543144159:transit-gateway/tgw-06910e97a1fbdf66a"), - To: PtrString("arn:aws:ec2:us-west-2:123456789012:transit-gateway-attachment/tgwa-1"), + From: new("arn:aws:ec2:us-east-2:986543144159:transit-gateway/tgw-06910e97a1fbdf66a"), + To: new("arn:aws:ec2:us-west-2:123456789012:transit-gateway-attachment/tgwa-1"), }, { - To: PtrString("arn:aws:ec2:us-east-2:986543144159:transit-gateway/tgw-06910e97a1fbdf66a"), - From: PtrString("arn:aws:ec2:us-west-2:123456789012:transit-gateway-attachment/tgwa-1"), + To: new("arn:aws:ec2:us-east-2:986543144159:transit-gateway/tgw-06910e97a1fbdf66a"), + From: new("arn:aws:ec2:us-west-2:123456789012:transit-gateway-attachment/tgwa-1"), }, // ec2-transit-gateway-route-table, ec2-transit-gateway-connect-peer { - From: PtrString("arn:aws:ec2:us-west-2:123456789012:transit-gateway-connect-peer/tgw-cnp-1"), - To: PtrString("arn:aws:ec2:us-east-2:986543144159:transit-gateway-route-table/tgw-rtb-043b7b4c0db1e4833"), + From: new("arn:aws:ec2:us-west-2:123456789012:transit-gateway-connect-peer/tgw-cnp-1"), + To: new("arn:aws:ec2:us-east-2:986543144159:transit-gateway-route-table/tgw-rtb-043b7b4c0db1e4833"), }, { - To: PtrString("arn:aws:ec2:us-west-2:123456789012:transit-gateway-connect-peer/tgw-cnp-1"), - From: PtrString("arn:aws:ec2:us-east-2:986543144159:transit-gateway-route-table/tgw-rtb-043b7b4c0db1e4833"), + To: new("arn:aws:ec2:us-west-2:123456789012:transit-gateway-connect-peer/tgw-cnp-1"), + From: new("arn:aws:ec2:us-east-2:986543144159:transit-gateway-route-table/tgw-rtb-043b7b4c0db1e4833"), }, // connection, ec2-vpn-connection { - From: PtrString("arn:aws:networkmanager:us-west-2:123456789012:connection/conn-1"), - To: PtrString("arn:aws:ec2:us-west-2:123456789012:vpn-connection/conn-1"), + From: new("arn:aws:networkmanager:us-west-2:123456789012:connection/conn-1"), + To: new("arn:aws:ec2:us-west-2:123456789012:vpn-connection/conn-1"), }, { - To: PtrString("arn:aws:networkmanager:us-west-2:123456789012:connection/conn-1"), - From: PtrString("arn:aws:ec2:us-west-2:123456789012:vpn-connection/conn-1"), + To: new("arn:aws:networkmanager:us-west-2:123456789012:connection/conn-1"), + From: new("arn:aws:ec2:us-west-2:123456789012:vpn-connection/conn-1"), }, }, }, 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 473d8e11..e8a4e46b 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 @@ -20,11 +20,11 @@ func TestSiteToSiteVpnAttachmentOutputMapper(t *testing.T) { name: "ok", item: &types.SiteToSiteVpnAttachment{ Attachment: &types.Attachment{ - AttachmentId: PtrString("stsa-1"), - CoreNetworkId: PtrString("cn-1"), + AttachmentId: new("stsa-1"), + CoreNetworkId: new("cn-1"), State: types.AttachmentStateAvailable, }, - VpnConnectionArn: PtrString("arn:aws:ec2:us-west-2:123456789012:vpn-connection/vpn-1234"), + VpnConnectionArn: new("arn:aws:ec2:us-west-2:123456789012:vpn-connection/vpn-1234"), }, expectedHealth: sdp.Health_HEALTH_OK, expectedAttr: "stsa-1", diff --git a/aws-source/adapters/networkmanager-site_test.go b/aws-source/adapters/networkmanager-site_test.go index 9c2e885f..72f2e90a 100644 --- a/aws-source/adapters/networkmanager-site_test.go +++ b/aws-source/adapters/networkmanager-site_test.go @@ -15,8 +15,8 @@ func TestSiteOutputMapper(t *testing.T) { output := networkmanager.GetSitesOutput{ Sites: []types.Site{ { - SiteId: PtrString("site1"), - GlobalNetworkId: PtrString("default"), + SiteId: new("site1"), + GlobalNetworkId: new("default"), }, }, } @@ -87,7 +87,7 @@ func TestSiteInputMapperSearch(t *testing.T) { name: "Valid networkmanager-site ARN", query: "arn:aws:networkmanager::123456789012:site/global-network-01231231231231231/site-444555aaabbb11223", expectedInput: &networkmanager.GetSitesInput{ - GlobalNetworkId: PtrString("global-network-01231231231231231"), + GlobalNetworkId: new("global-network-01231231231231231"), SiteIds: []string{"site-444555aaabbb11223"}, }, expectError: false, @@ -96,7 +96,7 @@ func TestSiteInputMapperSearch(t *testing.T) { name: "Global Network ID (backward compatibility)", query: "global-network-123456789", expectedInput: &networkmanager.GetSitesInput{ - GlobalNetworkId: PtrString("global-network-123456789"), + GlobalNetworkId: new("global-network-123456789"), }, expectError: false, }, 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 6db8a6c8..0f8a181b 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 @@ -24,11 +24,11 @@ func TestTransitGatewayConnectPeerAssociationsOutputMapper(t *testing.T) { out: networkmanager.GetTransitGatewayConnectPeerAssociationsOutput{ TransitGatewayConnectPeerAssociations: []types.TransitGatewayConnectPeerAssociation{ { - GlobalNetworkId: PtrString("default"), - TransitGatewayConnectPeerArn: PtrString("arn:aws:ec2:us-west-2:123456789012:transit-gateway-connect-peer-association/tgw-1234"), + GlobalNetworkId: new("default"), + TransitGatewayConnectPeerArn: new("arn:aws:ec2:us-west-2:123456789012:transit-gateway-connect-peer-association/tgw-1234"), State: types.TransitGatewayConnectPeerAssociationStateAvailable, - DeviceId: PtrString("device-1"), - LinkId: PtrString("link-1"), + DeviceId: new("device-1"), + LinkId: new("link-1"), }, }, }, diff --git a/aws-source/adapters/networkmanager-transit-gateway-peering_test.go b/aws-source/adapters/networkmanager-transit-gateway-peering_test.go index 03c8fd28..a09916df 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-peering_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-peering_test.go @@ -20,12 +20,12 @@ func TestTransitGatewayPeeringOutputMapper(t *testing.T) { name: "ok", item: &types.TransitGatewayPeering{ Peering: &types.Peering{ - PeeringId: PtrString("tgp-1"), - CoreNetworkId: PtrString("cn-1"), + PeeringId: new("tgp-1"), + CoreNetworkId: new("cn-1"), State: types.PeeringStateAvailable, }, - TransitGatewayArn: PtrString("arn:aws:ec2:us-west-2:123456789012:transit-gateway/tgw-1234"), - TransitGatewayPeeringAttachmentId: PtrString("gpa-1"), + TransitGatewayArn: new("arn:aws:ec2:us-west-2:123456789012:transit-gateway/tgw-1234"), + TransitGatewayPeeringAttachmentId: new("gpa-1"), }, expectedHealth: sdp.Health_HEALTH_OK, expectedAttr: "tgp-1", diff --git a/aws-source/adapters/networkmanager-transit-gateway-registration_test.go b/aws-source/adapters/networkmanager-transit-gateway-registration_test.go index 2b0fbeed..bbdd99f7 100644 --- a/aws-source/adapters/networkmanager-transit-gateway-registration_test.go +++ b/aws-source/adapters/networkmanager-transit-gateway-registration_test.go @@ -23,8 +23,8 @@ func TestTransitGatewayRegistrationOutputMapper(t *testing.T) { out: networkmanager.GetTransitGatewayRegistrationsOutput{ TransitGatewayRegistrations: []types.TransitGatewayRegistration{ { - GlobalNetworkId: PtrString("default"), - TransitGatewayArn: PtrString("arn:aws:ec2:us-west-2:123456789012:transit-gateway/tgw-1234"), + GlobalNetworkId: new("default"), + TransitGatewayArn: new("arn:aws:ec2:us-west-2:123456789012:transit-gateway/tgw-1234"), State: &types.TransitGatewayRegistrationStateReason{ Code: types.TransitGatewayRegistrationStateAvailable, }, @@ -52,8 +52,8 @@ func TestTransitGatewayRegistrationOutputMapper(t *testing.T) { out: networkmanager.GetTransitGatewayRegistrationsOutput{ TransitGatewayRegistrations: []types.TransitGatewayRegistration{ { - GlobalNetworkId: PtrString("default"), - TransitGatewayArn: PtrString("arn:aws:ec2:us-west-2:123456789012:transit-gateway/tgw-1234"), + GlobalNetworkId: new("default"), + TransitGatewayArn: new("arn:aws:ec2:us-west-2:123456789012:transit-gateway/tgw-1234"), State: &types.TransitGatewayRegistrationStateReason{ Code: types.TransitGatewayRegistrationStateDeleting, }, 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 92ada429..db9e6658 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 @@ -19,11 +19,11 @@ func TestTransitGatewayRouteTableAttachmentItemMapper(t *testing.T) { name: "ok", input: types.TransitGatewayRouteTableAttachment{ Attachment: &types.Attachment{ - AttachmentId: PtrString("attachment1"), - CoreNetworkId: PtrString("corenetwork1"), + AttachmentId: new("attachment1"), + CoreNetworkId: new("corenetwork1"), }, - TransitGatewayRouteTableArn: PtrString("arn:aws:ec2:us-west-2:123456789012:transit-gateway-route-table/tgw-rtb-9876543210123456"), - PeeringId: PtrString("peer1"), + TransitGatewayRouteTableArn: new("arn:aws:ec2:us-west-2:123456789012:transit-gateway-route-table/tgw-rtb-9876543210123456"), + PeeringId: new("peer1"), }, expectedAttr: "attachment1", tests: QueryTests{ @@ -51,8 +51,8 @@ func TestTransitGatewayRouteTableAttachmentItemMapper(t *testing.T) { name: "missing ec2-transit-gateway-route-table", input: types.TransitGatewayRouteTableAttachment{ Attachment: &types.Attachment{ - AttachmentId: PtrString("attachment1"), - CoreNetworkId: PtrString("corenetwork1"), + AttachmentId: new("attachment1"), + CoreNetworkId: new("corenetwork1"), }, }, expectedAttr: "attachment1", @@ -69,10 +69,10 @@ func TestTransitGatewayRouteTableAttachmentItemMapper(t *testing.T) { name: "invalid ec2-transit-gateway-route-table", input: types.TransitGatewayRouteTableAttachment{ Attachment: &types.Attachment{ - AttachmentId: PtrString("attachment1"), - CoreNetworkId: PtrString("corenetwork1"), + AttachmentId: new("attachment1"), + CoreNetworkId: new("corenetwork1"), }, - TransitGatewayRouteTableArn: PtrString("arn:aws:ec2:us-west-2:123456789012:transit-gateway-route-table-tgw-rtb-9876543210123456"), + TransitGatewayRouteTableArn: new("arn:aws:ec2:us-west-2:123456789012:transit-gateway-route-table-tgw-rtb-9876543210123456"), }, expectedAttr: "attachment1", tests: QueryTests{ diff --git a/aws-source/adapters/networkmanager-vpc-attachment_test.go b/aws-source/adapters/networkmanager-vpc-attachment_test.go index 0e9baa94..026a4696 100644 --- a/aws-source/adapters/networkmanager-vpc-attachment_test.go +++ b/aws-source/adapters/networkmanager-vpc-attachment_test.go @@ -10,8 +10,8 @@ import ( func TestVPCAttachmentItemMapper(t *testing.T) { input := types.VpcAttachment{ Attachment: &types.Attachment{ - AttachmentId: PtrString("attachment1"), - CoreNetworkId: PtrString("corenetwork1"), + AttachmentId: new("attachment1"), + CoreNetworkId: new("corenetwork1"), }, } scope := "123456789012.eu-west-2" 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 e02ff713..7bbc1b96 100644 --- a/aws-source/adapters/rds-db-cluster-parameter-group_test.go +++ b/aws-source/adapters/rds-db-cluster-parameter-group_test.go @@ -11,60 +11,60 @@ import ( func TestDBClusterParameterGroupOutputMapper(t *testing.T) { group := ClusterParameterGroup{ DBClusterParameterGroup: types.DBClusterParameterGroup{ - DBClusterParameterGroupName: PtrString("default.aurora-mysql5.7"), - DBParameterGroupFamily: PtrString("aurora-mysql5.7"), - Description: PtrString("Default cluster parameter group for aurora-mysql5.7"), - DBClusterParameterGroupArn: PtrString("arn:aws:rds:eu-west-1:052392120703:cluster-pg:default.aurora-mysql5.7"), + DBClusterParameterGroupName: new("default.aurora-mysql5.7"), + DBParameterGroupFamily: new("aurora-mysql5.7"), + Description: new("Default cluster parameter group for aurora-mysql5.7"), + DBClusterParameterGroupArn: new("arn:aws:rds:eu-west-1:052392120703:cluster-pg:default.aurora-mysql5.7"), }, Parameters: []types.Parameter{ { - ParameterName: PtrString("activate_all_roles_on_login"), - ParameterValue: PtrString("0"), - Description: PtrString("Automatically set all granted roles as active after the user has authenticated successfully."), - Source: PtrString("engine-default"), - ApplyType: PtrString("dynamic"), - DataType: PtrString("boolean"), - AllowedValues: PtrString("0,1"), - IsModifiable: PtrBool(true), + ParameterName: new("activate_all_roles_on_login"), + ParameterValue: new("0"), + Description: new("Automatically set all granted roles as active after the user has authenticated successfully."), + Source: new("engine-default"), + ApplyType: new("dynamic"), + DataType: new("boolean"), + AllowedValues: new("0,1"), + IsModifiable: new(true), ApplyMethod: types.ApplyMethodPendingReboot, SupportedEngineModes: []string{ "provisioned", }, }, { - ParameterName: PtrString("allow-suspicious-udfs"), - Description: PtrString("Controls whether user-defined functions that have only an xxx symbol for the main function can be loaded"), - Source: PtrString("engine-default"), - ApplyType: PtrString("static"), - DataType: PtrString("boolean"), - AllowedValues: PtrString("0,1"), - IsModifiable: PtrBool(false), + ParameterName: new("allow-suspicious-udfs"), + Description: new("Controls whether user-defined functions that have only an xxx symbol for the main function can be loaded"), + Source: new("engine-default"), + ApplyType: new("static"), + DataType: new("boolean"), + AllowedValues: new("0,1"), + IsModifiable: new(false), ApplyMethod: types.ApplyMethodPendingReboot, SupportedEngineModes: []string{ "provisioned", }, }, { - ParameterName: PtrString("aurora_binlog_replication_max_yield_seconds"), - Description: PtrString("Controls the number of seconds that binary log dump thread waits up to for the current binlog file to be filled by transactions. This wait period avoids contention that can arise from replicating each binlog event individually."), - Source: PtrString("engine-default"), - ApplyType: PtrString("dynamic"), - DataType: PtrString("integer"), - AllowedValues: PtrString("0-36000"), - IsModifiable: PtrBool(true), + ParameterName: new("aurora_binlog_replication_max_yield_seconds"), + Description: new("Controls the number of seconds that binary log dump thread waits up to for the current binlog file to be filled by transactions. This wait period avoids contention that can arise from replicating each binlog event individually."), + Source: new("engine-default"), + ApplyType: new("dynamic"), + DataType: new("integer"), + AllowedValues: new("0-36000"), + IsModifiable: new(true), ApplyMethod: types.ApplyMethodPendingReboot, SupportedEngineModes: []string{ "provisioned", }, }, { - ParameterName: PtrString("aurora_enable_staggered_replica_restart"), - Description: PtrString("Allow Aurora replicas to follow a staggered restart schedule to increase cluster availability."), - Source: PtrString("system"), - ApplyType: PtrString("dynamic"), - DataType: PtrString("boolean"), - AllowedValues: PtrString("0,1"), - IsModifiable: PtrBool(true), + ParameterName: new("aurora_enable_staggered_replica_restart"), + Description: new("Allow Aurora replicas to follow a staggered restart schedule to increase cluster availability."), + Source: new("system"), + ApplyType: new("dynamic"), + DataType: new("boolean"), + AllowedValues: new("0,1"), + IsModifiable: new(true), ApplyMethod: types.ApplyMethodImmediate, SupportedEngineModes: []string{ "provisioned", diff --git a/aws-source/adapters/rds-db-cluster_test.go b/aws-source/adapters/rds-db-cluster_test.go index 898fdfdc..6ce34ed7 100644 --- a/aws-source/adapters/rds-db-cluster_test.go +++ b/aws-source/adapters/rds-db-cluster_test.go @@ -15,115 +15,115 @@ func TestDBClusterOutputMapper(t *testing.T) { output := rds.DescribeDBClustersOutput{ DBClusters: []types.DBCluster{ { - AllocatedStorage: PtrInt32(100), + AllocatedStorage: new(int32(100)), AvailabilityZones: []string{ "eu-west-2c", // link }, - BackupRetentionPeriod: PtrInt32(7), - DBClusterIdentifier: PtrString("database-2"), - DBClusterParameterGroup: PtrString("default.postgres13"), - DBSubnetGroup: PtrString("default-vpc-0d7892e00e573e701"), // link - Status: PtrString("available"), - EarliestRestorableTime: PtrTime(time.Now()), - Endpoint: PtrString("database-2.cluster-camcztjohmlj.eu-west-2.rds.amazonaws.com"), // link - ReaderEndpoint: PtrString("database-2.cluster-ro-camcztjohmlj.eu-west-2.rds.amazonaws.com"), // link - MultiAZ: PtrBool(true), - Engine: PtrString("postgres"), - EngineVersion: PtrString("13.7"), - LatestRestorableTime: PtrTime(time.Now()), - Port: PtrInt32(5432), // link - MasterUsername: PtrString("postgres"), - PreferredBackupWindow: PtrString("04:48-05:18"), - PreferredMaintenanceWindow: PtrString("fri:04:05-fri:04:35"), + BackupRetentionPeriod: new(int32(7)), + DBClusterIdentifier: new("database-2"), + DBClusterParameterGroup: new("default.postgres13"), + DBSubnetGroup: new("default-vpc-0d7892e00e573e701"), // link + Status: new("available"), + EarliestRestorableTime: new(time.Now()), + Endpoint: new("database-2.cluster-camcztjohmlj.eu-west-2.rds.amazonaws.com"), // link + ReaderEndpoint: new("database-2.cluster-ro-camcztjohmlj.eu-west-2.rds.amazonaws.com"), // link + MultiAZ: new(true), + Engine: new("postgres"), + EngineVersion: new("13.7"), + LatestRestorableTime: new(time.Now()), + Port: new(int32(5432)), // link + MasterUsername: new("postgres"), + PreferredBackupWindow: new("04:48-05:18"), + PreferredMaintenanceWindow: new("fri:04:05-fri:04:35"), ReadReplicaIdentifiers: []string{ "arn:aws:rds:eu-west-1:052392120703:cluster:read-replica", // link }, DBClusterMembers: []types.DBClusterMember{ { - DBInstanceIdentifier: PtrString("database-2-instance-3"), // link - IsClusterWriter: PtrBool(false), - DBClusterParameterGroupStatus: PtrString("in-sync"), - PromotionTier: PtrInt32(1), + DBInstanceIdentifier: new("database-2-instance-3"), // link + IsClusterWriter: new(false), + DBClusterParameterGroupStatus: new("in-sync"), + PromotionTier: new(int32(1)), }, }, VpcSecurityGroups: []types.VpcSecurityGroupMembership{ { - VpcSecurityGroupId: PtrString("sg-094e151c9fc5da181"), // link - Status: PtrString("active"), + VpcSecurityGroupId: new("sg-094e151c9fc5da181"), // link + Status: new("active"), }, }, - HostedZoneId: PtrString("Z1TTGA775OQIYO"), // link - StorageEncrypted: PtrBool(true), - KmsKeyId: PtrString("arn:aws:kms:eu-west-2:052392120703:key/9653cbdd-1590-464a-8456-67389cef6933"), // link - DbClusterResourceId: PtrString("cluster-2EW4PDVN7F7V57CUJPYOEAA74M"), - DBClusterArn: PtrString("arn:aws:rds:eu-west-2:052392120703:cluster:database-2"), - IAMDatabaseAuthenticationEnabled: PtrBool(false), - ClusterCreateTime: PtrTime(time.Now()), - EngineMode: PtrString("provisioned"), - DeletionProtection: PtrBool(false), - HttpEndpointEnabled: PtrBool(false), + HostedZoneId: new("Z1TTGA775OQIYO"), // link + StorageEncrypted: new(true), + KmsKeyId: new("arn:aws:kms:eu-west-2:052392120703:key/9653cbdd-1590-464a-8456-67389cef6933"), // link + DbClusterResourceId: new("cluster-2EW4PDVN7F7V57CUJPYOEAA74M"), + DBClusterArn: new("arn:aws:rds:eu-west-2:052392120703:cluster:database-2"), + IAMDatabaseAuthenticationEnabled: new(false), + ClusterCreateTime: new(time.Now()), + EngineMode: new("provisioned"), + DeletionProtection: new(false), + HttpEndpointEnabled: new(false), ActivityStreamStatus: types.ActivityStreamStatusStopped, - CopyTagsToSnapshot: PtrBool(false), - CrossAccountClone: PtrBool(false), + CopyTagsToSnapshot: new(false), + CrossAccountClone: new(false), DomainMemberships: []types.DomainMembership{}, TagList: []types.Tag{}, - DBClusterInstanceClass: PtrString("db.m5d.large"), - StorageType: PtrString("io1"), - Iops: PtrInt32(1000), - PubliclyAccessible: PtrBool(true), - AutoMinorVersionUpgrade: PtrBool(true), - MonitoringInterval: PtrInt32(0), - PerformanceInsightsEnabled: PtrBool(false), - NetworkType: PtrString("IPV4"), - ActivityStreamKinesisStreamName: PtrString("aws-rds-das-db-AB1CDEFG23GHIJK4LMNOPQRST"), // link - ActivityStreamKmsKeyId: PtrString("ab12345e-1111-2bc3-12a3-ab1cd12345e"), // Not linking at the moment because there are too many possible formats. If you want to change this, submit a PR + DBClusterInstanceClass: new("db.m5d.large"), + StorageType: new("io1"), + Iops: new(int32(1000)), + PubliclyAccessible: new(true), + AutoMinorVersionUpgrade: new(true), + MonitoringInterval: new(int32(0)), + PerformanceInsightsEnabled: new(false), + NetworkType: new("IPV4"), + ActivityStreamKinesisStreamName: new("aws-rds-das-db-AB1CDEFG23GHIJK4LMNOPQRST"), // link + ActivityStreamKmsKeyId: new("ab12345e-1111-2bc3-12a3-ab1cd12345e"), // Not linking at the moment because there are too many possible formats. If you want to change this, submit a PR ActivityStreamMode: types.ActivityStreamModeAsync, - AutomaticRestartTime: PtrTime(time.Now()), + AutomaticRestartTime: new(time.Now()), AssociatedRoles: []types.DBClusterRole{}, // EC2 classic roles, ignore - BacktrackConsumedChangeRecords: PtrInt64(1), - BacktrackWindow: PtrInt64(2), - Capacity: PtrInt32(2), - CharacterSetName: PtrString("english"), - CloneGroupId: PtrString("id"), + BacktrackConsumedChangeRecords: new(int64(1)), + BacktrackWindow: new(int64(2)), + Capacity: new(int32(2)), + CharacterSetName: new("english"), + CloneGroupId: new("id"), CustomEndpoints: []string{ "endpoint1", // link dns }, DBClusterOptionGroupMemberships: []types.DBClusterOptionGroupStatus{ { - DBClusterOptionGroupName: PtrString("optionGroupName"), // link - Status: PtrString("good"), + DBClusterOptionGroupName: new("optionGroupName"), // link + Status: new("good"), }, }, - DBSystemId: PtrString("systemId"), - DatabaseName: PtrString("databaseName"), - EarliestBacktrackTime: PtrTime(time.Now()), + DBSystemId: new("systemId"), + DatabaseName: new("databaseName"), + EarliestBacktrackTime: new(time.Now()), EnabledCloudwatchLogsExports: []string{ "logExport1", }, - GlobalWriteForwardingRequested: PtrBool(true), + GlobalWriteForwardingRequested: new(true), GlobalWriteForwardingStatus: types.WriteForwardingStatusDisabled, MasterUserSecret: &types.MasterUserSecret{ - KmsKeyId: PtrString("arn:aws:kms:eu-west-2:052392120703:key/something"), // link - SecretArn: PtrString("arn:aws:service:region:account:type/id"), // link - SecretStatus: PtrString("okay"), + KmsKeyId: new("arn:aws:kms:eu-west-2:052392120703:key/something"), // link + SecretArn: new("arn:aws:service:region:account:type/id"), // link + SecretStatus: new("okay"), }, - MonitoringRoleArn: PtrString("arn:aws:service:region:account:type/id"), // link + MonitoringRoleArn: new("arn:aws:service:region:account:type/id"), // link PendingModifiedValues: &types.ClusterPendingModifiedValues{}, - PercentProgress: PtrString("99"), - PerformanceInsightsKMSKeyId: PtrString("arn:aws:service:region:account:type/id"), // link, assuming it's an ARN - PerformanceInsightsRetentionPeriod: PtrInt32(99), - ReplicationSourceIdentifier: PtrString("arn:aws:rds:eu-west-2:052392120703:cluster:database-1"), // link + PercentProgress: new("99"), + PerformanceInsightsKMSKeyId: new("arn:aws:service:region:account:type/id"), // link, assuming it's an ARN + PerformanceInsightsRetentionPeriod: new(int32(99)), + ReplicationSourceIdentifier: new("arn:aws:rds:eu-west-2:052392120703:cluster:database-1"), // link ScalingConfigurationInfo: &types.ScalingConfigurationInfo{ - AutoPause: PtrBool(true), - MaxCapacity: PtrInt32(10), - MinCapacity: PtrInt32(1), - SecondsBeforeTimeout: PtrInt32(10), - SecondsUntilAutoPause: PtrInt32(10), - TimeoutAction: PtrString("error"), + AutoPause: new(true), + MaxCapacity: new(int32(10)), + MinCapacity: new(int32(1)), + SecondsBeforeTimeout: new(int32(10)), + SecondsUntilAutoPause: new(int32(10)), + TimeoutAction: new("error"), }, ServerlessV2ScalingConfiguration: &types.ServerlessV2ScalingConfigurationInfo{ - MaxCapacity: PtrFloat64(10), - MinCapacity: PtrFloat64(1), + MaxCapacity: new(float64(10)), + MinCapacity: new(float64(1)), }, }, }, diff --git a/aws-source/adapters/rds-db-instance_test.go b/aws-source/adapters/rds-db-instance_test.go index 8ccd1176..0894bb00 100644 --- a/aws-source/adapters/rds-db-instance_test.go +++ b/aws-source/adapters/rds-db-instance_test.go @@ -15,156 +15,156 @@ func TestDBInstanceOutputMapper(t *testing.T) { output := &rds.DescribeDBInstancesOutput{ DBInstances: []types.DBInstance{ { - DBInstanceIdentifier: PtrString("database-1-instance-1"), - DBInstanceClass: PtrString("db.r6g.large"), - Engine: PtrString("aurora-mysql"), - DBInstanceStatus: PtrString("available"), - MasterUsername: PtrString("admin"), + DBInstanceIdentifier: new("database-1-instance-1"), + DBInstanceClass: new("db.r6g.large"), + Engine: new("aurora-mysql"), + DBInstanceStatus: new("available"), + MasterUsername: new("admin"), Endpoint: &types.Endpoint{ - Address: PtrString("database-1-instance-1.camcztjohmlj.eu-west-2.rds.amazonaws.com"), // link - Port: PtrInt32(3306), // link - HostedZoneId: PtrString("Z1TTGA775OQIYO"), // link + Address: new("database-1-instance-1.camcztjohmlj.eu-west-2.rds.amazonaws.com"), // link + Port: new(int32(3306)), // link + HostedZoneId: new("Z1TTGA775OQIYO"), // link }, - AllocatedStorage: PtrInt32(1), - InstanceCreateTime: PtrTime(time.Now()), - PreferredBackupWindow: PtrString("00:05-00:35"), - BackupRetentionPeriod: PtrInt32(1), + AllocatedStorage: new(int32(1)), + InstanceCreateTime: new(time.Now()), + PreferredBackupWindow: new("00:05-00:35"), + BackupRetentionPeriod: new(int32(1)), DBSecurityGroups: []types.DBSecurityGroupMembership{ { - DBSecurityGroupName: PtrString("name"), // This is EC2Classic only so we're skipping this + DBSecurityGroupName: new("name"), // This is EC2Classic only so we're skipping this }, }, VpcSecurityGroups: []types.VpcSecurityGroupMembership{ { - VpcSecurityGroupId: PtrString("sg-094e151c9fc5da181"), // link - Status: PtrString("active"), + VpcSecurityGroupId: new("sg-094e151c9fc5da181"), // link + Status: new("active"), }, }, DBParameterGroups: []types.DBParameterGroupStatus{ { - DBParameterGroupName: PtrString("default.aurora-mysql8.0"), // link - ParameterApplyStatus: PtrString("in-sync"), + DBParameterGroupName: new("default.aurora-mysql8.0"), // link + ParameterApplyStatus: new("in-sync"), }, }, - AvailabilityZone: PtrString("eu-west-2a"), // link + AvailabilityZone: new("eu-west-2a"), // link DBSubnetGroup: &types.DBSubnetGroup{ - DBSubnetGroupName: PtrString("default-vpc-0d7892e00e573e701"), // link - DBSubnetGroupDescription: PtrString("Created from the RDS Management Console"), - VpcId: PtrString("vpc-0d7892e00e573e701"), // link - SubnetGroupStatus: PtrString("Complete"), + DBSubnetGroupName: new("default-vpc-0d7892e00e573e701"), // link + DBSubnetGroupDescription: new("Created from the RDS Management Console"), + VpcId: new("vpc-0d7892e00e573e701"), // link + SubnetGroupStatus: new("Complete"), Subnets: []types.Subnet{ { - SubnetIdentifier: PtrString("subnet-0d8ae4b4e07647efa"), // lnk + SubnetIdentifier: new("subnet-0d8ae4b4e07647efa"), // lnk SubnetAvailabilityZone: &types.AvailabilityZone{ - Name: PtrString("eu-west-2b"), + Name: new("eu-west-2b"), }, SubnetOutpost: &types.Outpost{ - Arn: PtrString("arn:aws:service:region:account:type/id"), // link + Arn: new("arn:aws:service:region:account:type/id"), // link }, - SubnetStatus: PtrString("Active"), + SubnetStatus: new("Active"), }, }, }, - PreferredMaintenanceWindow: PtrString("fri:04:49-fri:05:19"), + PreferredMaintenanceWindow: new("fri:04:49-fri:05:19"), PendingModifiedValues: &types.PendingModifiedValues{}, - MultiAZ: PtrBool(false), - EngineVersion: PtrString("8.0.mysql_aurora.3.02.0"), - AutoMinorVersionUpgrade: PtrBool(true), + MultiAZ: new(false), + EngineVersion: new("8.0.mysql_aurora.3.02.0"), + AutoMinorVersionUpgrade: new(true), ReadReplicaDBInstanceIdentifiers: []string{ "read", }, - LicenseModel: PtrString("general-public-license"), + LicenseModel: new("general-public-license"), OptionGroupMemberships: []types.OptionGroupMembership{ { - OptionGroupName: PtrString("default:aurora-mysql-8-0"), - Status: PtrString("in-sync"), + OptionGroupName: new("default:aurora-mysql-8-0"), + Status: new("in-sync"), }, }, - PubliclyAccessible: PtrBool(false), - StorageType: PtrString("aurora"), - DbInstancePort: PtrInt32(0), - DBClusterIdentifier: PtrString("database-1"), // link - StorageEncrypted: PtrBool(true), - KmsKeyId: PtrString("arn:aws:kms:eu-west-2:052392120703:key/9653cbdd-1590-464a-8456-67389cef6933"), // link - DbiResourceId: PtrString("db-ET7CE5D5TQTK7MXNJGJNFQD52E"), - CACertificateIdentifier: PtrString("rds-ca-2019"), + PubliclyAccessible: new(false), + StorageType: new("aurora"), + DbInstancePort: new(int32(0)), + DBClusterIdentifier: new("database-1"), // link + StorageEncrypted: new(true), + KmsKeyId: new("arn:aws:kms:eu-west-2:052392120703:key/9653cbdd-1590-464a-8456-67389cef6933"), // link + DbiResourceId: new("db-ET7CE5D5TQTK7MXNJGJNFQD52E"), + CACertificateIdentifier: new("rds-ca-2019"), DomainMemberships: []types.DomainMembership{ { - Domain: PtrString("domain"), - FQDN: PtrString("fqdn"), - IAMRoleName: PtrString("role"), - Status: PtrString("enrolled"), + Domain: new("domain"), + FQDN: new("fqdn"), + IAMRoleName: new("role"), + Status: new("enrolled"), }, }, - CopyTagsToSnapshot: PtrBool(false), - MonitoringInterval: PtrInt32(60), - EnhancedMonitoringResourceArn: PtrString("arn:aws:logs:eu-west-2:052392120703:log-group:RDSOSMetrics:log-stream:db-ET7CE5D5TQTK7MXNJGJNFQD52E"), // link - MonitoringRoleArn: PtrString("arn:aws:iam::052392120703:role/rds-monitoring-role"), // link - PromotionTier: PtrInt32(1), - DBInstanceArn: PtrString("arn:aws:rds:eu-west-2:052392120703:db:database-1-instance-1"), - IAMDatabaseAuthenticationEnabled: PtrBool(false), - PerformanceInsightsEnabled: PtrBool(true), - PerformanceInsightsKMSKeyId: PtrString("arn:aws:kms:eu-west-2:052392120703:key/9653cbdd-1590-464a-8456-67389cef6933"), // link - PerformanceInsightsRetentionPeriod: PtrInt32(7), - DeletionProtection: PtrBool(false), + CopyTagsToSnapshot: new(false), + MonitoringInterval: new(int32(60)), + EnhancedMonitoringResourceArn: new("arn:aws:logs:eu-west-2:052392120703:log-group:RDSOSMetrics:log-stream:db-ET7CE5D5TQTK7MXNJGJNFQD52E"), // link + MonitoringRoleArn: new("arn:aws:iam::052392120703:role/rds-monitoring-role"), // link + PromotionTier: new(int32(1)), + DBInstanceArn: new("arn:aws:rds:eu-west-2:052392120703:db:database-1-instance-1"), + IAMDatabaseAuthenticationEnabled: new(false), + PerformanceInsightsEnabled: new(true), + PerformanceInsightsKMSKeyId: new("arn:aws:kms:eu-west-2:052392120703:key/9653cbdd-1590-464a-8456-67389cef6933"), // link + PerformanceInsightsRetentionPeriod: new(int32(7)), + DeletionProtection: new(false), AssociatedRoles: []types.DBInstanceRole{ { - FeatureName: PtrString("something"), - RoleArn: PtrString("arn:aws:service:region:account:type/id"), // link - Status: PtrString("associated"), + FeatureName: new("something"), + RoleArn: new("arn:aws:service:region:account:type/id"), // link + Status: new("associated"), }, }, TagList: []types.Tag{}, - CustomerOwnedIpEnabled: PtrBool(false), - BackupTarget: PtrString("region"), - NetworkType: PtrString("IPV4"), - StorageThroughput: PtrInt32(0), - ActivityStreamEngineNativeAuditFieldsIncluded: PtrBool(true), - ActivityStreamKinesisStreamName: PtrString("aws-rds-das-db-AB1CDEFG23GHIJK4LMNOPQRST"), // link - ActivityStreamKmsKeyId: PtrString("ab12345e-1111-2bc3-12a3-ab1cd12345e"), // Not linking at the moment because there are too many possible formats. If you want to change this, submit a PR + CustomerOwnedIpEnabled: new(false), + BackupTarget: new("region"), + NetworkType: new("IPV4"), + StorageThroughput: new(int32(0)), + ActivityStreamEngineNativeAuditFieldsIncluded: new(true), + ActivityStreamKinesisStreamName: new("aws-rds-das-db-AB1CDEFG23GHIJK4LMNOPQRST"), // link + ActivityStreamKmsKeyId: new("ab12345e-1111-2bc3-12a3-ab1cd12345e"), // Not linking at the moment because there are too many possible formats. If you want to change this, submit a PR ActivityStreamMode: types.ActivityStreamModeAsync, ActivityStreamPolicyStatus: types.ActivityStreamPolicyStatusLocked, ActivityStreamStatus: types.ActivityStreamStatusStarted, - AutomaticRestartTime: PtrTime(time.Now()), + AutomaticRestartTime: new(time.Now()), AutomationMode: types.AutomationModeAllPaused, - AwsBackupRecoveryPointArn: PtrString("arn:aws:service:region:account:type/id"), // link + AwsBackupRecoveryPointArn: new("arn:aws:service:region:account:type/id"), // link CertificateDetails: &types.CertificateDetails{ - CAIdentifier: PtrString("id"), - ValidTill: PtrTime(time.Now()), + CAIdentifier: new("id"), + ValidTill: new(time.Now()), }, - CharacterSetName: PtrString("something"), - CustomIamInstanceProfile: PtrString("arn:aws:service:region:account:type/id"), // link? + CharacterSetName: new("something"), + CustomIamInstanceProfile: new("arn:aws:service:region:account:type/id"), // link? DBInstanceAutomatedBackupsReplications: []types.DBInstanceAutomatedBackupsReplication{ { - DBInstanceAutomatedBackupsArn: PtrString("arn:aws:service:region:account:type/id"), // link + DBInstanceAutomatedBackupsArn: new("arn:aws:service:region:account:type/id"), // link }, }, - DBName: PtrString("name"), - DBSystemId: PtrString("id"), + DBName: new("name"), + DBSystemId: new("id"), EnabledCloudwatchLogsExports: []string{}, - Iops: PtrInt32(10), - LatestRestorableTime: PtrTime(time.Now()), + Iops: new(int32(10)), + LatestRestorableTime: new(time.Now()), ListenerEndpoint: &types.Endpoint{ - Address: PtrString("foo.bar.com"), // link - HostedZoneId: PtrString("id"), // link - Port: PtrInt32(5432), // link + Address: new("foo.bar.com"), // link + HostedZoneId: new("id"), // link + Port: new(int32(5432)), // link }, MasterUserSecret: &types.MasterUserSecret{ - KmsKeyId: PtrString("id"), // link - SecretArn: PtrString("arn:aws:service:region:account:type/id"), // link - SecretStatus: PtrString("okay"), + KmsKeyId: new("id"), // link + SecretArn: new("arn:aws:service:region:account:type/id"), // link + SecretStatus: new("okay"), }, - MaxAllocatedStorage: PtrInt32(10), - NcharCharacterSetName: PtrString("english"), + MaxAllocatedStorage: new(int32(10)), + NcharCharacterSetName: new("english"), ProcessorFeatures: []types.ProcessorFeature{}, ReadReplicaDBClusterIdentifiers: []string{}, - ReadReplicaSourceDBInstanceIdentifier: PtrString("id"), + ReadReplicaSourceDBInstanceIdentifier: new("id"), ReplicaMode: types.ReplicaModeMounted, - ResumeFullAutomationModeTime: PtrTime(time.Now()), - SecondaryAvailabilityZone: PtrString("eu-west-1"), // link + ResumeFullAutomationModeTime: new(time.Now()), + SecondaryAvailabilityZone: new("eu-west-1"), // link StatusInfos: []types.DBInstanceStatusInfo{}, - TdeCredentialArn: PtrString("arn:aws:service:region:account:type/id"), // I don't have a good example for this so skipping for now. PR if required - Timezone: PtrString("GB"), + TdeCredentialArn: new("arn:aws:service:region:account:type/id"), // I don't have a good example for this so skipping for now. PR if required + Timezone: new("GB"), }, }, } diff --git a/aws-source/adapters/rds-db-parameter-group_test.go b/aws-source/adapters/rds-db-parameter-group_test.go index e07c9cc9..24c7606a 100644 --- a/aws-source/adapters/rds-db-parameter-group_test.go +++ b/aws-source/adapters/rds-db-parameter-group_test.go @@ -11,51 +11,51 @@ import ( func TestDBParameterGroupOutputMapper(t *testing.T) { group := ParameterGroup{ DBParameterGroup: types.DBParameterGroup{ - DBParameterGroupName: PtrString("default.aurora-mysql5.7"), - DBParameterGroupFamily: PtrString("aurora-mysql5.7"), - Description: PtrString("Default parameter group for aurora-mysql5.7"), - DBParameterGroupArn: PtrString("arn:aws:rds:eu-west-1:052392120703:pg:default.aurora-mysql5.7"), + DBParameterGroupName: new("default.aurora-mysql5.7"), + DBParameterGroupFamily: new("aurora-mysql5.7"), + Description: new("Default parameter group for aurora-mysql5.7"), + DBParameterGroupArn: new("arn:aws:rds:eu-west-1:052392120703:pg:default.aurora-mysql5.7"), }, Parameters: []types.Parameter{ { - ParameterName: PtrString("activate_all_roles_on_login"), - ParameterValue: PtrString("0"), - Description: PtrString("Automatically set all granted roles as active after the user has authenticated successfully."), - Source: PtrString("engine-default"), - ApplyType: PtrString("dynamic"), - DataType: PtrString("boolean"), - AllowedValues: PtrString("0,1"), - IsModifiable: PtrBool(true), + ParameterName: new("activate_all_roles_on_login"), + ParameterValue: new("0"), + Description: new("Automatically set all granted roles as active after the user has authenticated successfully."), + Source: new("engine-default"), + ApplyType: new("dynamic"), + DataType: new("boolean"), + AllowedValues: new("0,1"), + IsModifiable: new(true), ApplyMethod: types.ApplyMethodPendingReboot, }, { - ParameterName: PtrString("allow-suspicious-udfs"), - Description: PtrString("Controls whether user-defined functions that have only an xxx symbol for the main function can be loaded"), - Source: PtrString("engine-default"), - ApplyType: PtrString("static"), - DataType: PtrString("boolean"), - AllowedValues: PtrString("0,1"), - IsModifiable: PtrBool(false), + ParameterName: new("allow-suspicious-udfs"), + Description: new("Controls whether user-defined functions that have only an xxx symbol for the main function can be loaded"), + Source: new("engine-default"), + ApplyType: new("static"), + DataType: new("boolean"), + AllowedValues: new("0,1"), + IsModifiable: new(false), ApplyMethod: types.ApplyMethodPendingReboot, }, { - ParameterName: PtrString("aurora_parallel_query"), - Description: PtrString("This parameter can be used to enable and disable Aurora Parallel Query."), - Source: PtrString("engine-default"), - ApplyType: PtrString("dynamic"), - DataType: PtrString("boolean"), - AllowedValues: PtrString("0,1"), - IsModifiable: PtrBool(true), + ParameterName: new("aurora_parallel_query"), + Description: new("This parameter can be used to enable and disable Aurora Parallel Query."), + Source: new("engine-default"), + ApplyType: new("dynamic"), + DataType: new("boolean"), + AllowedValues: new("0,1"), + IsModifiable: new(true), ApplyMethod: types.ApplyMethodPendingReboot, }, { - ParameterName: PtrString("autocommit"), - Description: PtrString("Sets the autocommit mode"), - Source: PtrString("engine-default"), - ApplyType: PtrString("dynamic"), - DataType: PtrString("boolean"), - AllowedValues: PtrString("0,1"), - IsModifiable: PtrBool(true), + ParameterName: new("autocommit"), + Description: new("Sets the autocommit mode"), + Source: new("engine-default"), + ApplyType: new("dynamic"), + DataType: new("boolean"), + AllowedValues: new("0,1"), + IsModifiable: new(true), ApplyMethod: types.ApplyMethodPendingReboot, }, }, diff --git a/aws-source/adapters/rds-db-subnet-group_test.go b/aws-source/adapters/rds-db-subnet-group_test.go index 7b7db1dc..fad5cc36 100644 --- a/aws-source/adapters/rds-db-subnet-group_test.go +++ b/aws-source/adapters/rds-db-subnet-group_test.go @@ -15,23 +15,23 @@ func TestDBSubnetGroupOutputMapper(t *testing.T) { output := rds.DescribeDBSubnetGroupsOutput{ DBSubnetGroups: []types.DBSubnetGroup{ { - DBSubnetGroupName: PtrString("default-vpc-0d7892e00e573e701"), - DBSubnetGroupDescription: PtrString("Created from the RDS Management Console"), - VpcId: PtrString("vpc-0d7892e00e573e701"), // link - SubnetGroupStatus: PtrString("Complete"), + DBSubnetGroupName: new("default-vpc-0d7892e00e573e701"), + DBSubnetGroupDescription: new("Created from the RDS Management Console"), + VpcId: new("vpc-0d7892e00e573e701"), // link + SubnetGroupStatus: new("Complete"), Subnets: []types.Subnet{ { - SubnetIdentifier: PtrString("subnet-0450a637af9984235"), // link + SubnetIdentifier: new("subnet-0450a637af9984235"), // link SubnetAvailabilityZone: &types.AvailabilityZone{ - Name: PtrString("eu-west-2c"), // link + Name: new("eu-west-2c"), // link }, SubnetOutpost: &types.Outpost{ - Arn: PtrString("arn:aws:service:region:account:type/id"), // link + Arn: new("arn:aws:service:region:account:type/id"), // link }, - SubnetStatus: PtrString("Active"), + SubnetStatus: new("Active"), }, }, - DBSubnetGroupArn: PtrString("arn:aws:rds:eu-west-2:052392120703:subgrp:default-vpc-0d7892e00e573e701"), + DBSubnetGroupArn: new("arn:aws:rds:eu-west-2:052392120703:subgrp:default-vpc-0d7892e00e573e701"), SupportedNetworkTypes: []string{ "IPV4", }, diff --git a/aws-source/adapters/rds-option-group_test.go b/aws-source/adapters/rds-option-group_test.go index 53fc62a2..fa493476 100644 --- a/aws-source/adapters/rds-option-group_test.go +++ b/aws-source/adapters/rds-option-group_test.go @@ -12,13 +12,13 @@ func TestOptionGroupOutputMapper(t *testing.T) { output := rds.DescribeOptionGroupsOutput{ OptionGroupsList: []types.OptionGroup{ { - OptionGroupName: PtrString("default:aurora-mysql-8-0"), - OptionGroupDescription: PtrString("Default option group for aurora-mysql 8.0"), - EngineName: PtrString("aurora-mysql"), - MajorEngineVersion: PtrString("8.0"), + OptionGroupName: new("default:aurora-mysql-8-0"), + OptionGroupDescription: new("Default option group for aurora-mysql 8.0"), + EngineName: new("aurora-mysql"), + MajorEngineVersion: new("8.0"), Options: []types.Option{}, - AllowsVpcAndNonVpcInstanceMemberships: PtrBool(true), - OptionGroupArn: PtrString("arn:aws:rds:eu-west-2:052392120703:og:default:aurora-mysql-8-0"), + AllowsVpcAndNonVpcInstanceMemberships: new(true), + OptionGroupArn: new("arn:aws:rds:eu-west-2:052392120703:og:default:aurora-mysql-8-0"), }, }, } diff --git a/aws-source/adapters/rds.go b/aws-source/adapters/rds.go index f7aaff05..47dcfb33 100644 --- a/aws-source/adapters/rds.go +++ b/aws-source/adapters/rds.go @@ -33,8 +33,8 @@ func (m mockRdsClient) ListTagsForResource(ctx context.Context, params *rds.List return &rds.ListTagsForResourceOutput{ TagList: []types.Tag{ { - Key: PtrString("key"), - Value: PtrString("value"), + Key: new("key"), + Value: new("value"), }, }, }, nil diff --git a/aws-source/adapters/route53-health-check_test.go b/aws-source/adapters/route53-health-check_test.go index 35939600..91e98f33 100644 --- a/aws-source/adapters/route53-health-check_test.go +++ b/aws-source/adapters/route53-health-check_test.go @@ -12,37 +12,37 @@ import ( func TestHealthCheckItemMapper(t *testing.T) { hc := HealthCheck{ HealthCheck: types.HealthCheck{ - Id: PtrString("d7ce5d72-6d1f-4147-8246-d0ca3fb505d6"), - CallerReference: PtrString("85d56b3f-873c-498b-a2dd-554ec13c5289"), + Id: new("d7ce5d72-6d1f-4147-8246-d0ca3fb505d6"), + CallerReference: new("85d56b3f-873c-498b-a2dd-554ec13c5289"), HealthCheckConfig: &types.HealthCheckConfig{ - IPAddress: PtrString("1.1.1.1"), - Port: PtrInt32(443), + IPAddress: new("1.1.1.1"), + Port: new(int32(443)), Type: types.HealthCheckTypeHttps, - FullyQualifiedDomainName: PtrString("one.one.one.one"), - RequestInterval: PtrInt32(30), - FailureThreshold: PtrInt32(3), - MeasureLatency: PtrBool(false), - Inverted: PtrBool(false), - Disabled: PtrBool(false), - EnableSNI: PtrBool(true), + FullyQualifiedDomainName: new("one.one.one.one"), + RequestInterval: new(int32(30)), + FailureThreshold: new(int32(3)), + MeasureLatency: new(false), + Inverted: new(false), + Disabled: new(false), + EnableSNI: new(true), }, - HealthCheckVersion: PtrInt64(1), + HealthCheckVersion: new(int64(1)), }, HealthCheckObservations: []types.HealthCheckObservation{ { Region: types.HealthCheckRegionApNortheast1, - IPAddress: PtrString("15.177.62.21"), + IPAddress: new("15.177.62.21"), StatusReport: &types.StatusReport{ - Status: PtrString("Success: HTTP Status Code 200, OK"), - CheckedTime: PtrTime(time.Now()), + Status: new("Success: HTTP Status Code 200, OK"), + CheckedTime: new(time.Now()), }, }, { Region: types.HealthCheckRegionEuWest1, - IPAddress: PtrString("15.177.10.21"), + IPAddress: new("15.177.10.21"), StatusReport: &types.StatusReport{ - Status: PtrString("Failure: Connection timed out. The endpoint or the internet connection is down, or requests are being blocked by your firewall. See https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover-router-firewall-rules.html"), - CheckedTime: PtrTime(time.Now()), + Status: new("Failure: Connection timed out. The endpoint or the internet connection is down, or requests are being blocked by your firewall. See https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/dns-failover-router-firewall-rules.html"), + CheckedTime: new(time.Now()), }, }, }, diff --git a/aws-source/adapters/route53-hosted-zone_test.go b/aws-source/adapters/route53-hosted-zone_test.go index da48d7de..11f35f1d 100644 --- a/aws-source/adapters/route53-hosted-zone_test.go +++ b/aws-source/adapters/route53-hosted-zone_test.go @@ -11,17 +11,17 @@ import ( func TestHostedZoneItemMapper(t *testing.T) { zone := types.HostedZone{ - Id: PtrString("/hostedzone/Z08416862SZP5DJXIDB29"), - Name: PtrString("overmind-demo.com."), - CallerReference: PtrString("RISWorkflow-RD:144d3779-1574-42bf-9e75-f309838ea0a1"), + Id: new("/hostedzone/Z08416862SZP5DJXIDB29"), + Name: new("overmind-demo.com."), + CallerReference: new("RISWorkflow-RD:144d3779-1574-42bf-9e75-f309838ea0a1"), Config: &types.HostedZoneConfig{ - Comment: PtrString("HostedZone created by Route53 Registrar"), + Comment: new("HostedZone created by Route53 Registrar"), PrivateZone: false, }, - ResourceRecordSetCount: PtrInt64(3), + ResourceRecordSetCount: new(int64(3)), LinkedService: &types.LinkedService{ - Description: PtrString("service description"), - ServicePrincipal: PtrString("principal"), + Description: new("service description"), + ServicePrincipal: new("principal"), }, } diff --git a/aws-source/adapters/route53-resource-record-set_test.go b/aws-source/adapters/route53-resource-record-set_test.go index d18c15a9..8a12ec22 100644 --- a/aws-source/adapters/route53-resource-record-set_test.go +++ b/aws-source/adapters/route53-resource-record-set_test.go @@ -15,50 +15,50 @@ import ( func TestResourceRecordSetItemMapper(t *testing.T) { recordSet := types.ResourceRecordSet{ - Name: PtrString("overmind-demo.com."), + Name: new("overmind-demo.com."), Type: types.RRTypeNs, - TTL: PtrInt64(172800), + TTL: new(int64(172800)), GeoProximityLocation: &types.GeoProximityLocation{ - AWSRegion: PtrString("us-east-1"), - Bias: PtrInt32(100), + AWSRegion: new("us-east-1"), + Bias: new(int32(100)), Coordinates: &types.Coordinates{}, - LocalZoneGroup: PtrString("group"), + LocalZoneGroup: new("group"), }, ResourceRecords: []types.ResourceRecord{ { - Value: PtrString("ns-1673.awsdns-17.co.uk."), // link + Value: new("ns-1673.awsdns-17.co.uk."), // link }, { - Value: PtrString("ns-1505.awsdns-60.org."), // link + Value: new("ns-1505.awsdns-60.org."), // link }, { - Value: PtrString("ns-955.awsdns-55.net."), // link + Value: new("ns-955.awsdns-55.net."), // link }, { - Value: PtrString("ns-276.awsdns-34.com."), // link + Value: new("ns-276.awsdns-34.com."), // link }, }, AliasTarget: &types.AliasTarget{ - DNSName: PtrString("foo.bar.com"), // link + DNSName: new("foo.bar.com"), // link EvaluateTargetHealth: true, - HostedZoneId: PtrString("id"), + HostedZoneId: new("id"), }, CidrRoutingConfig: &types.CidrRoutingConfig{ - CollectionId: PtrString("id"), - LocationName: PtrString("somewhere"), + CollectionId: new("id"), + LocationName: new("somewhere"), }, Failover: types.ResourceRecordSetFailoverPrimary, GeoLocation: &types.GeoLocation{ - ContinentCode: PtrString("GB"), - CountryCode: PtrString("GB"), - SubdivisionCode: PtrString("ENG"), + ContinentCode: new("GB"), + CountryCode: new("GB"), + SubdivisionCode: new("ENG"), }, - HealthCheckId: PtrString("id"), // link - MultiValueAnswer: PtrBool(true), + HealthCheckId: new("id"), // link + MultiValueAnswer: new(true), Region: types.ResourceRecordSetRegionApEast1, - SetIdentifier: PtrString("identifier"), - TrafficPolicyInstanceId: PtrString("id"), - Weight: PtrInt64(100), + SetIdentifier: new("identifier"), + TrafficPolicyInstanceId: new("id"), + Weight: new(int64(100)), } item, err := resourceRecordSetItemMapper("", "foo", &recordSet) diff --git a/aws-source/adapters/s3.go b/aws-source/adapters/s3.go index 2e187d5e..c4cef606 100644 --- a/aws-source/adapters/s3.go +++ b/aws-source/adapters/s3.go @@ -214,7 +214,7 @@ func getImpl(ctx context.Context, cache sdpcache.Cache, client S3Client, scope s var wg sync.WaitGroup var err error - bucketName := PtrString(query) + bucketName := new(query) location, err = client.GetBucketLocation(ctx, &s3.GetBucketLocationInput{ Bucket: bucketName, diff --git a/aws-source/adapters/s3_test.go b/aws-source/adapters/s3_test.go index 574c87be..42682f47 100644 --- a/aws-source/adapters/s3_test.go +++ b/aws-source/adapters/s3_test.go @@ -160,8 +160,8 @@ func TestS3SourceCaching(t *testing.T) { } var owner = types.Owner{ - DisplayName: PtrString("dylan"), - ID: PtrString("id"), + DisplayName: new("dylan"), + ID: new("id"), } // TestS3Client A client that returns example data @@ -171,8 +171,8 @@ func (t TestS3Client) ListBuckets(ctx context.Context, params *s3.ListBucketsInp return &s3.ListBucketsOutput{ Buckets: []types.Bucket{ { - CreationDate: PtrTime(time.Now()), - Name: PtrString("foo"), + CreationDate: new(time.Now()), + Name: new("foo"), }, }, Owner: &owner, @@ -185,10 +185,10 @@ func (t TestS3Client) GetBucketAcl(ctx context.Context, params *s3.GetBucketAclI { Grantee: &types.Grantee{ Type: types.TypeAmazonCustomerByEmail, - DisplayName: PtrString("dylan"), - EmailAddress: PtrString("dylan@company.com"), - ID: PtrString("id"), - URI: PtrString("uri"), + DisplayName: new("dylan"), + EmailAddress: new("dylan@company.com"), + ID: new("id"), + URI: new("uri"), }, }, }, @@ -199,15 +199,15 @@ func (t TestS3Client) GetBucketAcl(ctx context.Context, params *s3.GetBucketAclI func (t TestS3Client) GetBucketAnalyticsConfiguration(ctx context.Context, params *s3.GetBucketAnalyticsConfigurationInput, optFns ...func(*s3.Options)) (*s3.GetBucketAnalyticsConfigurationOutput, error) { return &s3.GetBucketAnalyticsConfigurationOutput{ AnalyticsConfiguration: &types.AnalyticsConfiguration{ - Id: PtrString("id"), + Id: new("id"), StorageClassAnalysis: &types.StorageClassAnalysis{ DataExport: &types.StorageClassAnalysisDataExport{ Destination: &types.AnalyticsExportDestination{ S3BucketDestination: &types.AnalyticsS3BucketDestination{ - Bucket: PtrString("arn:aws:s3:::amzn-s3-demo-bucket"), + Bucket: new("arn:aws:s3:::amzn-s3-demo-bucket"), Format: types.AnalyticsS3ExportFileFormatCsv, - BucketAccountId: PtrString("id"), - Prefix: PtrString("pre"), + BucketAccountId: new("id"), + Prefix: new("pre"), }, }, OutputSchemaVersion: types.StorageClassAnalysisSchemaVersionV1, @@ -233,8 +233,8 @@ func (t TestS3Client) GetBucketCors(ctx context.Context, params *s3.GetBucketCor ExposeHeaders: []string{ "foo", }, - ID: PtrString("id"), - MaxAgeSeconds: PtrInt32(10), + ID: new("id"), + MaxAgeSeconds: new(int32(10)), }, }, }, nil @@ -247,9 +247,9 @@ func (t TestS3Client) GetBucketEncryption(ctx context.Context, params *s3.GetBuc { ApplyServerSideEncryptionByDefault: &types.ServerSideEncryptionByDefault{ SSEAlgorithm: types.ServerSideEncryptionAes256, - KMSMasterKeyID: PtrString("id"), + KMSMasterKeyID: new("id"), }, - BucketKeyEnabled: PtrBool(true), + BucketKeyEnabled: new(true), }, }, }, @@ -259,12 +259,12 @@ func (t TestS3Client) GetBucketEncryption(ctx context.Context, params *s3.GetBuc func (t TestS3Client) GetBucketIntelligentTieringConfiguration(ctx context.Context, params *s3.GetBucketIntelligentTieringConfigurationInput, optFns ...func(*s3.Options)) (*s3.GetBucketIntelligentTieringConfigurationOutput, error) { return &s3.GetBucketIntelligentTieringConfigurationOutput{ IntelligentTieringConfiguration: &types.IntelligentTieringConfiguration{ - Id: PtrString("id"), + Id: new("id"), Status: types.IntelligentTieringStatusEnabled, Tierings: []types.Tiering{ { AccessTier: types.IntelligentTieringAccessTierDeepArchiveAccess, - Days: PtrInt32(100), + Days: new(int32(100)), }, }, Filter: &types.IntelligentTieringFilter{}, @@ -277,20 +277,20 @@ func (t TestS3Client) GetBucketInventoryConfiguration(ctx context.Context, param InventoryConfiguration: &types.InventoryConfiguration{ Destination: &types.InventoryDestination{ S3BucketDestination: &types.InventoryS3BucketDestination{ - Bucket: PtrString("arn:aws:s3:::amzn-s3-demo-bucket"), + Bucket: new("arn:aws:s3:::amzn-s3-demo-bucket"), Format: types.InventoryFormatCsv, - AccountId: PtrString("id"), + AccountId: new("id"), Encryption: &types.InventoryEncryption{ SSEKMS: &types.SSEKMS{ - KeyId: PtrString("key"), + KeyId: new("key"), }, }, - Prefix: PtrString("pre"), + Prefix: new("pre"), }, }, - Id: PtrString("id"), + Id: new("id"), IncludedObjectVersions: types.InventoryIncludedObjectVersionsAll, - IsEnabled: PtrBool(true), + IsEnabled: new(true), Schedule: &types.InventorySchedule{ Frequency: types.InventoryFrequencyDaily, }, @@ -304,30 +304,30 @@ func (t TestS3Client) GetBucketLifecycleConfiguration(ctx context.Context, param { Status: types.ExpirationStatusEnabled, AbortIncompleteMultipartUpload: &types.AbortIncompleteMultipartUpload{ - DaysAfterInitiation: PtrInt32(1), + DaysAfterInitiation: new(int32(1)), }, Expiration: &types.LifecycleExpiration{ - Date: PtrTime(time.Now()), - Days: PtrInt32(3), - ExpiredObjectDeleteMarker: PtrBool(true), + Date: new(time.Now()), + Days: new(int32(3)), + ExpiredObjectDeleteMarker: new(true), }, - ID: PtrString("id"), + ID: new("id"), NoncurrentVersionExpiration: &types.NoncurrentVersionExpiration{ - NewerNoncurrentVersions: PtrInt32(3), - NoncurrentDays: PtrInt32(1), + NewerNoncurrentVersions: new(int32(3)), + NoncurrentDays: new(int32(1)), }, NoncurrentVersionTransitions: []types.NoncurrentVersionTransition{ { - NewerNoncurrentVersions: PtrInt32(1), - NoncurrentDays: PtrInt32(1), + NewerNoncurrentVersions: new(int32(1)), + NoncurrentDays: new(int32(1)), StorageClass: types.TransitionStorageClassGlacierIr, }, }, - Prefix: PtrString("pre"), + Prefix: new("pre"), Transitions: []types.Transition{ { - Date: PtrTime(time.Now()), - Days: PtrInt32(12), + Date: new(time.Now()), + Days: new(int32(12)), StorageClass: types.TransitionStorageClassGlacierIr, }, }, @@ -345,13 +345,13 @@ func (t TestS3Client) GetBucketLocation(ctx context.Context, params *s3.GetBucke func (t TestS3Client) GetBucketLogging(ctx context.Context, params *s3.GetBucketLoggingInput, optFns ...func(*s3.Options)) (*s3.GetBucketLoggingOutput, error) { return &s3.GetBucketLoggingOutput{ LoggingEnabled: &types.LoggingEnabled{ - TargetBucket: PtrString("bucket"), - TargetPrefix: PtrString("pre"), + TargetBucket: new("bucket"), + TargetPrefix: new("pre"), TargetGrants: []types.TargetGrant{ { Grantee: &types.Grantee{ Type: types.TypeGroup, - ID: PtrString("id"), + ID: new("id"), }, }, }, @@ -362,7 +362,7 @@ func (t TestS3Client) GetBucketLogging(ctx context.Context, params *s3.GetBucket func (t TestS3Client) GetBucketMetricsConfiguration(ctx context.Context, params *s3.GetBucketMetricsConfigurationInput, optFns ...func(*s3.Options)) (*s3.GetBucketMetricsConfigurationOutput, error) { return &s3.GetBucketMetricsConfigurationOutput{ MetricsConfiguration: &types.MetricsConfiguration{ - Id: PtrString("id"), + Id: new("id"), }, }, nil } @@ -372,43 +372,43 @@ func (t TestS3Client) GetBucketNotificationConfiguration(ctx context.Context, pa LambdaFunctionConfigurations: []types.LambdaFunctionConfiguration{ { Events: []types.Event{}, - LambdaFunctionArn: PtrString("arn:partition:service:region:account-id:resource-type:resource-id"), - Id: PtrString("id"), + LambdaFunctionArn: new("arn:partition:service:region:account-id:resource-type:resource-id"), + Id: new("id"), }, }, EventBridgeConfiguration: &types.EventBridgeConfiguration{}, QueueConfigurations: []types.QueueConfiguration{ { Events: []types.Event{}, - QueueArn: PtrString("arn:partition:service:region:account-id:resource-type:resource-id"), + QueueArn: new("arn:partition:service:region:account-id:resource-type:resource-id"), Filter: &types.NotificationConfigurationFilter{ Key: &types.S3KeyFilter{ FilterRules: []types.FilterRule{ { Name: types.FilterRuleNamePrefix, - Value: PtrString("foo"), + Value: new("foo"), }, }, }, }, - Id: PtrString("id"), + Id: new("id"), }, }, TopicConfigurations: []types.TopicConfiguration{ { Events: []types.Event{}, - TopicArn: PtrString("arn:partition:service:region:account-id:resource-type:resource-id"), + TopicArn: new("arn:partition:service:region:account-id:resource-type:resource-id"), Filter: &types.NotificationConfigurationFilter{ Key: &types.S3KeyFilter{ FilterRules: []types.FilterRule{ { Name: types.FilterRuleNameSuffix, - Value: PtrString("fix"), + Value: new("fix"), }, }, }, }, - Id: PtrString("id"), + Id: new("id"), }, }, }, nil @@ -428,14 +428,14 @@ func (t TestS3Client) GetBucketOwnershipControls(ctx context.Context, params *s3 func (t TestS3Client) GetBucketPolicy(ctx context.Context, params *s3.GetBucketPolicyInput, optFns ...func(*s3.Options)) (*s3.GetBucketPolicyOutput, error) { return &s3.GetBucketPolicyOutput{ - Policy: PtrString("policy"), + Policy: new("policy"), }, nil } func (t TestS3Client) GetBucketPolicyStatus(ctx context.Context, params *s3.GetBucketPolicyStatusInput, optFns ...func(*s3.Options)) (*s3.GetBucketPolicyStatusOutput, error) { return &s3.GetBucketPolicyStatusOutput{ PolicyStatus: &types.PolicyStatus{ - IsPublic: PtrBool(true), + IsPublic: new(true), }, }, nil } @@ -443,28 +443,28 @@ func (t TestS3Client) GetBucketPolicyStatus(ctx context.Context, params *s3.GetB func (t TestS3Client) GetBucketReplication(ctx context.Context, params *s3.GetBucketReplicationInput, optFns ...func(*s3.Options)) (*s3.GetBucketReplicationOutput, error) { return &s3.GetBucketReplicationOutput{ ReplicationConfiguration: &types.ReplicationConfiguration{ - Role: PtrString("role"), + Role: new("role"), Rules: []types.ReplicationRule{ { Destination: &types.Destination{ - Bucket: PtrString("bucket"), + Bucket: new("bucket"), AccessControlTranslation: &types.AccessControlTranslation{ Owner: types.OwnerOverrideDestination, }, - Account: PtrString("account"), + Account: new("account"), EncryptionConfiguration: &types.EncryptionConfiguration{ - ReplicaKmsKeyID: PtrString("keyId"), + ReplicaKmsKeyID: new("keyId"), }, Metrics: &types.Metrics{ Status: types.MetricsStatusEnabled, EventThreshold: &types.ReplicationTimeValue{ - Minutes: PtrInt32(1), + Minutes: new(int32(1)), }, }, ReplicationTime: &types.ReplicationTime{ Status: types.ReplicationTimeStatusEnabled, Time: &types.ReplicationTimeValue{ - Minutes: PtrInt32(1), + Minutes: new(int32(1)), }, }, StorageClass: types.StorageClassGlacier, @@ -497,23 +497,23 @@ func (t TestS3Client) GetBucketVersioning(ctx context.Context, params *s3.GetBuc func (t TestS3Client) GetBucketWebsite(ctx context.Context, params *s3.GetBucketWebsiteInput, optFns ...func(*s3.Options)) (*s3.GetBucketWebsiteOutput, error) { return &s3.GetBucketWebsiteOutput{ ErrorDocument: &types.ErrorDocument{ - Key: PtrString("key"), + Key: new("key"), }, IndexDocument: &types.IndexDocument{ - Suffix: PtrString("html"), + Suffix: new("html"), }, RedirectAllRequestsTo: &types.RedirectAllRequestsTo{ - HostName: PtrString("hostname"), + HostName: new("hostname"), Protocol: types.ProtocolHttps, }, RoutingRules: []types.RoutingRule{ { Redirect: &types.Redirect{ - HostName: PtrString("hostname"), - HttpRedirectCode: PtrString("303"), + HostName: new("hostname"), + HttpRedirectCode: new("303"), Protocol: types.ProtocolHttp, - ReplaceKeyPrefixWith: PtrString("pre"), - ReplaceKeyWith: PtrString("key"), + ReplaceKeyPrefixWith: new("pre"), + ReplaceKeyWith: new("key"), }, }, }, diff --git a/aws-source/adapters/sns-data-protection-policy.go b/aws-source/adapters/sns-data-protection-policy.go index 5d54efe7..3fc23916 100644 --- a/aws-source/adapters/sns-data-protection-policy.go +++ b/aws-source/adapters/sns-data-protection-policy.go @@ -28,7 +28,7 @@ func getDataProtectionPolicyFunc(ctx context.Context, client dataProtectionPolic } // ResourceArn is the topic ARN that the policy is associated with - attr := map[string]interface{}{ + attr := map[string]any{ "TopicArn": *input.ResourceArn, } @@ -64,7 +64,7 @@ func NewSNSDataProtectionPolicyAdapter(client dataProtectionPolicyClient, accoun Region: region, DisableList: true, AdapterMetadata: dataProtectionPolicyAdapterMetadata, - cache: cache, + cache: cache, GetInputMapper: func(scope, query string) *sns.GetDataProtectionPolicyInput { return &sns.GetDataProtectionPolicyInput{ ResourceArn: &query, diff --git a/aws-source/adapters/sns-data-protection-policy_test.go b/aws-source/adapters/sns-data-protection-policy_test.go index 3e3fe366..1066d7a9 100644 --- a/aws-source/adapters/sns-data-protection-policy_test.go +++ b/aws-source/adapters/sns-data-protection-policy_test.go @@ -13,7 +13,7 @@ type mockDataProtectionPolicyClient struct{} func (m mockDataProtectionPolicyClient) GetDataProtectionPolicy(ctx context.Context, params *sns.GetDataProtectionPolicyInput, optFns ...func(*sns.Options)) (*sns.GetDataProtectionPolicyOutput, error) { return &sns.GetDataProtectionPolicyOutput{ - DataProtectionPolicy: PtrString("{\"Name\":\"data_protection_policy\",\"Description\":\"Example data protection policy\",\"Version\":\"2021-06-01\",\"Statement\":[{\"DataDirection\":\"Inbound\",\"Principal\":[\"*\"],\"DataIdentifier\":[\"arn:aws:dataprotection::aws:data-identifier/CreditCardNumber\"],\"Operation\":{\"Deny\":{}}}]}"), + DataProtectionPolicy: new("{\"Name\":\"data_protection_policy\",\"Description\":\"Example data protection policy\",\"Version\":\"2021-06-01\",\"Statement\":[{\"DataDirection\":\"Inbound\",\"Principal\":[\"*\"],\"DataIdentifier\":[\"arn:aws:dataprotection::aws:data-identifier/CreditCardNumber\"],\"Operation\":{\"Deny\":{}}}]}"), }, nil } @@ -22,7 +22,7 @@ func TestGetDataProtectionPolicyFunc(t *testing.T) { cli := &mockDataProtectionPolicyClient{} item, err := getDataProtectionPolicyFunc(ctx, cli, "scope", &sns.GetDataProtectionPolicyInput{ - ResourceArn: PtrString("arn:aws:sns:us-east-1:123456789012:mytopic"), + ResourceArn: new("arn:aws:sns:us-east-1:123456789012:mytopic"), }) if err != nil { t.Fatal(err) diff --git a/aws-source/adapters/sns-endpoint_test.go b/aws-source/adapters/sns-endpoint_test.go index b26f5b65..8b40aded 100644 --- a/aws-source/adapters/sns-endpoint_test.go +++ b/aws-source/adapters/sns-endpoint_test.go @@ -44,7 +44,7 @@ func TestGetEndpointFunc(t *testing.T) { cli := &mockEndpointClient{} item, err := getEndpointFunc(ctx, cli, "scope", &sns.GetEndpointAttributesInput{ - EndpointArn: PtrString("arn:aws:sns:us-west-2:123456789012:endpoint/GCM/MyApplication/12345678-abcd-9012-efgh-345678901234"), + EndpointArn: new("arn:aws:sns:us-west-2:123456789012:endpoint/GCM/MyApplication/12345678-abcd-9012-efgh-345678901234"), }) if err != nil { t.Fatal(err) diff --git a/aws-source/adapters/sns-platform-application_test.go b/aws-source/adapters/sns-platform-application_test.go index f7fffbd6..fb821785 100644 --- a/aws-source/adapters/sns-platform-application_test.go +++ b/aws-source/adapters/sns-platform-application_test.go @@ -15,8 +15,8 @@ type mockPlatformApplicationClient struct{} func (m mockPlatformApplicationClient) ListTagsForResource(ctx context.Context, input *sns.ListTagsForResourceInput, f ...func(*sns.Options)) (*sns.ListTagsForResourceOutput, error) { return &sns.ListTagsForResourceOutput{ Tags: []types.Tag{ - {Key: PtrString("tag1"), Value: PtrString("value1")}, - {Key: PtrString("tag2"), Value: PtrString("value2")}, + {Key: new("tag1"), Value: new("value1")}, + {Key: new("tag2"), Value: new("value2")}, }, }, nil } @@ -34,14 +34,14 @@ func (m mockPlatformApplicationClient) ListPlatformApplications(ctx context.Cont return &sns.ListPlatformApplicationsOutput{ PlatformApplications: []types.PlatformApplication{ { - PlatformApplicationArn: PtrString("arn:aws:sns:us-west-2:123456789012:app/ADM/MyApplication"), + PlatformApplicationArn: new("arn:aws:sns:us-west-2:123456789012:app/ADM/MyApplication"), Attributes: map[string]string{ "SuccessFeedbackSampleRate": "100", "Enabled": "true", }, }, { - PlatformApplicationArn: PtrString("arn:aws:sns:us-west-2:123456789012:app/MPNS/MyOtherApplication"), + PlatformApplicationArn: new("arn:aws:sns:us-west-2:123456789012:app/MPNS/MyOtherApplication"), Attributes: map[string]string{ "SuccessFeedbackSampleRate": "100", "Enabled": "true", @@ -56,7 +56,7 @@ func TestGetPlatformApplicationFunc(t *testing.T) { cli := mockPlatformApplicationClient{} item, err := getPlatformApplicationFunc(ctx, cli, "scope", &sns.GetPlatformApplicationAttributesInput{ - PlatformApplicationArn: PtrString("arn:aws:sns:us-west-2:123456789012:my-topic"), + PlatformApplicationArn: new("arn:aws:sns:us-west-2:123456789012:my-topic"), }) if err != nil { t.Fatal(err) diff --git a/aws-source/adapters/sns-subscription_test.go b/aws-source/adapters/sns-subscription_test.go index d83d419f..bd8af04f 100644 --- a/aws-source/adapters/sns-subscription_test.go +++ b/aws-source/adapters/sns-subscription_test.go @@ -29,11 +29,11 @@ func (t snsTestClient) ListSubscriptions(context.Context, *sns.ListSubscriptions return &sns.ListSubscriptionsOutput{ Subscriptions: []types.Subscription{ { - Owner: PtrString("123456789012"), - Endpoint: PtrString("my-email@example.com"), - Protocol: PtrString("email"), - TopicArn: PtrString("arn:aws:sns:us-west-2:123456789012:my-topic"), - SubscriptionArn: PtrString("arn:aws:sns:us-west-2:123456789012:my-topic:8a21d249-4329-4871-acc6-7be709c6ea7f"), + Owner: new("123456789012"), + Endpoint: new("my-email@example.com"), + Protocol: new("email"), + TopicArn: new("arn:aws:sns:us-west-2:123456789012:my-topic"), + SubscriptionArn: new("arn:aws:sns:us-west-2:123456789012:my-topic:8a21d249-4329-4871-acc6-7be709c6ea7f"), }, }, }, nil @@ -42,8 +42,8 @@ func (t snsTestClient) ListSubscriptions(context.Context, *sns.ListSubscriptions func (t snsTestClient) ListTagsForResource(context.Context, *sns.ListTagsForResourceInput, ...func(*sns.Options)) (*sns.ListTagsForResourceOutput, error) { return &sns.ListTagsForResourceOutput{ Tags: []types.Tag{ - {Key: PtrString("tag1"), Value: PtrString("value1")}, - {Key: PtrString("tag2"), Value: PtrString("value2")}, + {Key: new("tag1"), Value: new("value1")}, + {Key: new("tag2"), Value: new("value2")}, }, }, nil } @@ -53,7 +53,7 @@ func TestSNSGetFunc(t *testing.T) { cli := snsTestClient{} item, err := getSubsFunc(ctx, cli, "scope", &sns.GetSubscriptionAttributesInput{ - SubscriptionArn: PtrString("arn:aws:sns:us-west-2:123456789012:my-topic:8a21d249-4329-4871-acc6-7be709c6ea7f"), + SubscriptionArn: new("arn:aws:sns:us-west-2:123456789012:my-topic:8a21d249-4329-4871-acc6-7be709c6ea7f"), }) if err != nil { t.Fatal(err) diff --git a/aws-source/adapters/sns-topic_test.go b/aws-source/adapters/sns-topic_test.go index 57f344de..d0208bf0 100644 --- a/aws-source/adapters/sns-topic_test.go +++ b/aws-source/adapters/sns-topic_test.go @@ -30,7 +30,7 @@ func (t testTopicClient) ListTopics(context.Context, *sns.ListTopicsInput, ...fu return &sns.ListTopicsOutput{ Topics: []types.Topic{ { - TopicArn: PtrString("arn:aws:sns:us-west-2:123456789012:my-topic"), + TopicArn: new("arn:aws:sns:us-west-2:123456789012:my-topic"), }, }, }, nil @@ -39,8 +39,8 @@ func (t testTopicClient) ListTopics(context.Context, *sns.ListTopicsInput, ...fu func (t testTopicClient) ListTagsForResource(context.Context, *sns.ListTagsForResourceInput, ...func(*sns.Options)) (*sns.ListTagsForResourceOutput, error) { return &sns.ListTagsForResourceOutput{ Tags: []types.Tag{ - {Key: PtrString("tag1"), Value: PtrString("value1")}, - {Key: PtrString("tag2"), Value: PtrString("value2")}, + {Key: new("tag1"), Value: new("value1")}, + {Key: new("tag2"), Value: new("value2")}, }, }, nil } @@ -50,7 +50,7 @@ func TestGetTopicFunc(t *testing.T) { cli := testTopicClient{} item, err := getTopicFunc(ctx, cli, "scope", &sns.GetTopicAttributesInput{ - TopicArn: PtrString("arn:aws:sns:us-west-2:123456789012:my-topic"), + TopicArn: new("arn:aws:sns:us-west-2:123456789012:my-topic"), }) if err != nil { t.Fatal(err) diff --git a/aws-source/adapters/sqs-queue.go b/aws-source/adapters/sqs-queue.go index 7283bf7d..71ade1e4 100644 --- a/aws-source/adapters/sqs-queue.go +++ b/aws-source/adapters/sqs-queue.go @@ -98,7 +98,7 @@ func sqsQueueSearchInputMapper(scope string, query string) (*sqs.GetQueueAttribu } return &sqs.GetQueueAttributesInput{ - QueueUrl: PtrString(fmt.Sprintf("https://sqs.%s.%s/%s/%s", arn.Region, GetPartitionDNSSuffix(arn.Partition), arn.AccountID, arn.Resource)), + QueueUrl: new(fmt.Sprintf("https://sqs.%s.%s/%s/%s", arn.Region, GetPartitionDNSSuffix(arn.Partition), arn.AccountID, arn.Resource)), AttributeNames: []types.QueueAttributeName{"All"}, }, nil } @@ -111,7 +111,7 @@ func NewSQSQueueAdapter(client sqsClient, accountID string, region string, cache Region: region, ListInput: &sqs.ListQueuesInput{}, AdapterMetadata: sqsQueueAdapterMetadata, - cache: cache, + cache: cache, GetInputMapper: func(scope, query string) *sqs.GetQueueAttributesInput { return &sqs.GetQueueAttributesInput{ QueueUrl: &query, diff --git a/aws-source/adapters/sqs-queue_test.go b/aws-source/adapters/sqs-queue_test.go index 11524d97..449769fd 100644 --- a/aws-source/adapters/sqs-queue_test.go +++ b/aws-source/adapters/sqs-queue_test.go @@ -54,7 +54,7 @@ func TestGetFunc(t *testing.T) { cli := testClient{} item, err := getFunc(ctx, cli, "scope", &sqs.GetQueueAttributesInput{ - QueueUrl: PtrString("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue"), + QueueUrl: new("https://sqs.us-west-2.amazonaws.com/123456789012/MyQueue"), }) if err != nil { t.Fatal(err) diff --git a/aws-source/adapters/ssm-parameter.go b/aws-source/adapters/ssm-parameter.go index 0294d5a3..434c4d3f 100644 --- a/aws-source/adapters/ssm-parameter.go +++ b/aws-source/adapters/ssm-parameter.go @@ -186,7 +186,7 @@ func ssmParameterOutputMapper(ctx context.Context, client ssmClient, scope strin if parameter.Type != types.ParameterTypeSecureString { request := &ssm.GetParameterInput{ Name: parameter.Name, - WithDecryption: PtrBool(false), // let's be double sure we don't get any secrets + WithDecryption: new(false), // let's be double sure we don't get any secrets } paramResp, err := client.GetParameter(ctx, request) if err != nil { @@ -231,8 +231,8 @@ func NewSSMParameterAdapter(client ssmClient, accountID string, region string, c return &ssm.DescribeParametersInput{ ParameterFilters: []types.ParameterStringFilter{ { - Key: PtrString("Name"), - Option: PtrString("Equals"), + Key: new("Name"), + Option: new("Equals"), Values: []string{query}, }, }, diff --git a/cmd/changes_submit_plan.go b/cmd/changes_submit_plan.go index ab802f82..d3d8171d 100644 --- a/cmd/changes_submit_plan.go +++ b/cmd/changes_submit_plan.go @@ -52,7 +52,7 @@ func changeTitle(ctx context.Context, arg string) string { return arg } - describeBytes, err := exec.CommandContext(ctx, "git", "describe", "--long").Output() + describeBytes, err := exec.CommandContext(ctx, "git", "describe", "--long").Output() //nolint:gosec // G702: all arguments are hardcoded string literals; no user input reaches this command describe := strings.TrimSpace(string(describeBytes)) if err != nil { log.WithError(err).Trace("failed to run 'git describe' for default title") diff --git a/cmd/pterm.go b/cmd/pterm.go index 3818ec37..0b533d76 100644 --- a/cmd/pterm.go +++ b/cmd/pterm.go @@ -42,7 +42,7 @@ func PTermSetup() { // disrupting bubbletea rendering (and potentially getting overwritten). // Otherwise, when TEABUG is set, log to a file. if len(os.Getenv("TEABUG")) > 0 { - f, err := os.OpenFile("teabug.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) //nolint:gomnd + f, err := os.OpenFile("teabug.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) if err != nil { fmt.Println("fatal:", err) os.Exit(1) @@ -151,7 +151,7 @@ func RunRevlinkWarmup(ctx context.Context, oi sdp.OvermindInstance, postPlanPrin } func RunPlan(ctx context.Context, args []string) error { - c := exec.CommandContext(ctx, "terraform", args...) + c := exec.CommandContext(ctx, "terraform", args...) //nolint:gosec // G702: args are CLI arguments from the local user who invoked this command; this tool runs on the user's own machine // remove go's default process cancel behaviour, so that terraform has a // chance to gracefully shutdown when ^C is pressed. Otherwise the @@ -180,7 +180,7 @@ func RunPlan(ctx context.Context, args []string) error { } func RunApply(ctx context.Context, args []string) error { - c := exec.CommandContext(ctx, "terraform", args...) + c := exec.CommandContext(ctx, "terraform", args...) //nolint:gosec // G702: args are CLI arguments from the local user who invoked this command; this tool runs on the user's own machine // remove go's default process cancel behaviour, so that terraform has a // chance to gracefully shutdown when ^C is pressed. Otherwise the diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index 045a1c33..2218fe33 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -115,7 +115,7 @@ func TerraformPlanImpl(ctx context.Context, cmd *cobra.Command, oi sdp.OvermindI // Convert provided plan into JSON for easier parsing /////////////////////////////////////////////////////////////////// - tfPlanJsonCmd := exec.CommandContext(ctx, "terraform", "show", "-json", planFile) + tfPlanJsonCmd := exec.CommandContext(ctx, "terraform", "show", "-json", planFile) //nolint:gosec // G702: "terraform", "show", "-json" are hardcoded; planFile is from the local user's CLI -out flag tfPlanJsonCmd.Stderr = multi.NewWriter() // send output through PTerm; is usually empty @@ -181,7 +181,7 @@ func TerraformPlanImpl(ctx context.Context, cmd *cobra.Command, oi sdp.OvermindI } line := printer.Sprintf("%v (%v)", mapping.TerraformName, mapping.Message) - _, err = fmt.Fprintf(resourceExtractionResults, " %v\n", line) + _, err = fmt.Fprintf(resourceExtractionResults, " %v\n", line) //nolint:gosec // G203: resourceExtractionResults is a pterm.MultiPrinter writer (terminal UI), not an http.ResponseWriter; no XSS vector if err != nil { return fmt.Errorf("error writing to resource extraction results: %w", err) } @@ -229,7 +229,7 @@ func TerraformPlanImpl(ctx context.Context, cmd *cobra.Command, oi sdp.OvermindI } title := changeTitle(ctx, viper.GetString("title")) - tfPlanTextCmd := exec.CommandContext(ctx, "terraform", "show", planFile) + tfPlanTextCmd := exec.CommandContext(ctx, "terraform", "show", planFile) //nolint:gosec // G702: "terraform" and "show" are hardcoded; planFile is from the local user's CLI -out flag tfPlanTextCmd.Stderr = multi.NewWriter() // send output through PTerm; is usually empty @@ -460,7 +460,7 @@ retryLoop: // getTicketLinkFromPlan reads the plan file to create a unique hash to identify this change func getTicketLinkFromPlan(planFile string) (string, error) { - plan, err := os.ReadFile(planFile) + plan, err := os.ReadFile(planFile) //nolint:gosec // G703: planFile is from the local user's CLI args; reading their chosen file is the intended behavior of this CLI tool if err != nil { return "", fmt.Errorf("failed to read plan file (%v): %w", planFile, err) } diff --git a/cmd/theme.go b/cmd/theme.go index 5758f0a4..e5c71cfa 100644 --- a/cmd/theme.go +++ b/cmd/theme.go @@ -103,21 +103,21 @@ func MarkdownStyle() ansi.StyleConfig { BlockSuffix: "\n", Color: getHex(ColorPalette.LabelBase), }, - Indent: ptrUint(2), + Indent: new(uint(2)), }, BlockQuote: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - Italic: ptrBool(true), + Italic: new(true), }, - Indent: ptrUint(1), - IndentToken: ptrString("│ "), + Indent: new(uint(1)), + IndentToken: new("│ "), }, List: ansi.StyleList{ LevelIndent: 2, }, Heading: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - Bold: ptrBool(true), + Bold: new(true), Color: getHex(ColorPalette.LabelTitle), BlockSuffix: "\n", }, @@ -146,17 +146,17 @@ func MarkdownStyle() ansi.StyleConfig { H6: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ Prefix: "###### ", - Bold: ptrBool(false), + Bold: new(false), }, }, Strikethrough: ansi.StylePrimitive{ - CrossedOut: ptrBool(true), + CrossedOut: new(true), }, Emph: ansi.StylePrimitive{ - Italic: ptrBool(true), + Italic: new(true), }, Strong: ansi.StylePrimitive{ - Bold: ptrBool(true), + Bold: new(true), }, HorizontalRule: ansi.StylePrimitive{ Color: getHex(ColorPalette.LabelBase), @@ -174,16 +174,16 @@ func MarkdownStyle() ansi.StyleConfig { }, Link: ansi.StylePrimitive{ Color: getHex(ColorPalette.LabelLink), - Underline: ptrBool(true), + Underline: new(true), BlockPrefix: "(", BlockSuffix: ")", }, LinkText: ansi.StylePrimitive{ - Bold: ptrBool(true), + Bold: new(true), }, Image: ansi.StylePrimitive{ Color: getHex(ColorPalette.LabelLink), - Underline: ptrBool(true), + Underline: new(true), BlockPrefix: "(", BlockSuffix: ")", }, @@ -192,14 +192,14 @@ func MarkdownStyle() ansi.StyleConfig { }, CodeBlock: ansi.StyleCodeBlock{ StyleBlock: ansi.StyleBlock{ - Margin: ptrUint(4), + Margin: new(uint(4)), }, Theme: "solarized-light", }, Table: ansi.StyleTable{ - CenterSeparator: ptrString("┼"), - ColumnSeparator: ptrString("│"), - RowSeparator: ptrString("─"), + CenterSeparator: new("┼"), + ColumnSeparator: new("│"), + RowSeparator: new("─"), }, DefinitionDescription: ansi.StylePrimitive{ BlockPrefix: "\n🠶 ", @@ -270,16 +270,6 @@ func IndentSymbol() string { return " " } -func ptrBool(b bool) *bool { - return &b -} -func ptrUint(u uint) *uint { - return &u -} -func ptrString(s string) *string { - return &s -} - func getHex(c color.Color) *string { r, g, b, _ := c.RGBA() // RGBA returns values in 0-65535, convert to 0-255 diff --git a/cmd/version_check.go b/cmd/version_check.go index 1be29d5f..62ac39fd 100644 --- a/cmd/version_check.go +++ b/cmd/version_check.go @@ -55,7 +55,7 @@ func checkVersion(ctx context.Context, currentVersion string) (latestVersion str req.Header.Set("User-Agent", fmt.Sprintf("overmind-cli/%s", currentVersion)) req.Header.Set("Accept", "application/vnd.github.v3+json") - resp, err := client.Do(req) + resp, err := client.Do(req) //nolint:gosec // G704: URL is the hardcoded constant githubReleasesURL; no user input reaches the request URL if err != nil { log.WithError(err).Debug("Failed to check for CLI updates") return "", false diff --git a/go.mod b/go.mod index 1e238062..0013ef1f 100644 --- a/go.mod +++ b/go.mod @@ -196,7 +196,6 @@ require ( 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 @@ -505,6 +504,7 @@ require ( 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 + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect modernc.org/libc v1.67.6 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect diff --git a/go/auth/auth.go b/go/auth/auth.go index 9b7e2ce2..21f37347 100644 --- a/go/auth/auth.go +++ b/go/auth/auth.go @@ -70,7 +70,7 @@ type ClientCredentialsConfig struct { // The ClientID of the application that we'll be authenticating as ClientID string // ClientSecret that corresponds to the ClientID - ClientSecret string + ClientSecret string //nolint:gosec // G101 (hardcoded secret): config field name, not a credential value; never JSON-marshaled into logs or responses } type TokenSourceOptionsFunc func(*clientcredentials.Config) @@ -128,7 +128,7 @@ func (flowConfig ClientCredentialsConfig) TokenSource(ctx context.Context, oAuth type Auth0Config struct { Domain string ClientID string - ClientSecret string + ClientSecret string //nolint:gosec // G101 (hardcoded secret): config field name, not a credential value; populated from env vars and only used in OAuth token exchange Audience string } @@ -298,7 +298,7 @@ func (n *natsTokenClient) Sign(in []byte) ([]byte, error) { // tokens type APIKeyTokenSource struct { // The API Key to use to authenticate to the Overmind API - ApiKey string + ApiKey string //nolint:gosec // G101 (hardcoded secret): config field name, not a credential value; only passed to API key exchange endpoint token *oauth2.Token apiKeyClient sdpconnect.ApiKeyServiceClient } diff --git a/go/auth/middleware.go b/go/auth/middleware.go index 5a806c55..0241af4a 100644 --- a/go/auth/middleware.go +++ b/go/auth/middleware.go @@ -7,6 +7,7 @@ import ( "net/http" "net/url" "regexp" + "slices" "strings" "time" @@ -483,13 +484,7 @@ type CustomClaims struct { // 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 + return slices.Contains(result, expectedScope) } // Validate does nothing for this example, but we need diff --git a/go/auth/middleware_test.go b/go/auth/middleware_test.go index 0e18fd26..b0231711 100644 --- a/go/auth/middleware_test.go +++ b/go/auth/middleware_test.go @@ -122,8 +122,7 @@ func TestNewAuthMiddleware(t *testing.T) { t.Fatal(err) } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() jwksURL := server.Start(ctx) @@ -699,8 +698,7 @@ func TestConnectErrorHandling(t *testing.T) { t.Fatal(err) } - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() + ctx := t.Context() jwksURL := server.Start(ctx) diff --git a/go/auth/nats.go b/go/auth/nats.go index 40c0aade..cb8a8eaf 100644 --- a/go/auth/nats.go +++ b/go/auth/nats.go @@ -224,10 +224,7 @@ func (o NATSOptions) Connect() (sdp.EncodedConnection, error) { triesLeft-- } // Log a non-negative value: 0 means unlimited retries (NumRetries < 0) - logTriesLeft := triesLeft - if logTriesLeft < 0 { - logTriesLeft = 0 - } + logTriesLeft := max(triesLeft, 0) lf := log.Fields{ "servers": servers, "triesLeft": logTriesLeft, diff --git a/go/discovery/cmd.go b/go/discovery/cmd.go index c2b4fe7f..7ad794a7 100644 --- a/go/discovery/cmd.go +++ b/go/discovery/cmd.go @@ -206,7 +206,7 @@ func MapFromEngineConfig(ec *EngineConfig) map[string]any { sourceAccessToken = "[REDACTED]" } - return map[string]interface{}{ + return map[string]any{ "engine-type": ec.EngineType, "version": ec.Version, "source-name": ec.SourceName, diff --git a/go/discovery/engine.go b/go/discovery/engine.go index 1b64388e..2b3df9db 100644 --- a/go/discovery/engine.go +++ b/go/discovery/engine.go @@ -61,7 +61,7 @@ type EngineConfig struct { // 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" + ApiKey string //nolint:gosec // G101 (hardcoded secret): config field name, not a credential value; populated from CLI flags/env vars // 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 diff --git a/go/discovery/engine_initerror_test.go b/go/discovery/engine_initerror_test.go index 26acfa34..97e1ff07 100644 --- a/go/discovery/engine_initerror_test.go +++ b/go/discovery/engine_initerror_test.go @@ -89,13 +89,11 @@ func TestInitErrorConcurrentAccess(t *testing.T) { // Readers for range 10 { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { for range iterations { _ = e.GetInitError() } - }() + }) } wg.Wait() diff --git a/go/discovery/getfindmutex_test.go b/go/discovery/getfindmutex_test.go index ff7b4916..e854f5e4 100644 --- a/go/discovery/getfindmutex_test.go +++ b/go/discovery/getfindmutex_test.go @@ -149,14 +149,11 @@ func TestGetLock(t *testing.T) { }() - actionWG.Add(1) - - go func() { + actionWG.Go(func() { for action := range actionChan { order = append(order, action) } - actionWG.Done() - }() + }) go func(t *testing.T) { wg.Wait() diff --git a/go/discovery/performance_test.go b/go/discovery/performance_test.go index fd521c71..2b9075aa 100644 --- a/go/discovery/performance_test.go +++ b/go/discovery/performance_test.go @@ -42,7 +42,7 @@ func (s *SlowAdapter) Hidden() bool { 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{}{ + attributes, _ := sdp.ToAttributes(map[string]any{ "name": query, }) diff --git a/go/discovery/querytracker_test.go b/go/discovery/querytracker_test.go index 338e29f8..7d4a8d0f 100644 --- a/go/discovery/querytracker_test.go +++ b/go/discovery/querytracker_test.go @@ -272,11 +272,9 @@ func TestCancel(t *testing.T) { var wg sync.WaitGroup var err error - wg.Add(1) - go func() { + wg.Go(func() { items, edges, _, err = qt.Execute(context.Background()) - wg.Done() - }() + }) // Give it some time to populate the cancelFunc time.Sleep(100 * time.Millisecond) diff --git a/go/sdp-go/instance_detect.go b/go/sdp-go/instance_detect.go index 2da3ac9e..9f4ef8a5 100644 --- a/go/sdp-go/instance_detect.go +++ b/go/sdp-go/instance_detect.go @@ -58,7 +58,7 @@ func NewOvermindInstance(ctx context.Context, app string) (OvermindInstance, err } req = req.WithContext(ctx) - res, err := tracing.HTTPClient().Do(req) + res, err := tracing.HTTPClient().Do(req) //nolint:gosec // G107 (SSRF): URL is built from the app base URL (CLI config) + hardcoded path /api/public/instance-data if err != nil { return OvermindInstance{}, fmt.Errorf("could not fetch instance-data: %w", err) } diff --git a/go/sdp-go/items.go b/go/sdp-go/items.go index 49f7f59b..5f91e1e0 100644 --- a/go/sdp-go/items.go +++ b/go/sdp-go/items.go @@ -23,7 +23,7 @@ const WILDCARD = "*" // 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 value any var err error value, err = i.GetAttributes().Get(i.GetUniqueAttribute()) @@ -65,7 +65,7 @@ func (i *Item) GloballyUniqueName() string { // 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())))) + return HashSum((fmt.Append(nil, i.GloballyUniqueName()))) } // IsEqual compares two Edges for equality by checking the From reference @@ -78,7 +78,7 @@ func (e *Edge) IsEqual(other *Edge) bool { // 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())))) + return HashSum((fmt.Append(nil, r.GloballyUniqueName()))) } // GloballyUniqueName Returns a string that defines the Item globally. This a @@ -172,17 +172,17 @@ func (r *Reference) ToQuery() *Query { // 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{} +func (a *ItemAttributes) Get(name string) (any, error) { + var result any // Start at the beginning of the map, we will then traverse down as required result = a.GetAttrStruct().AsMap() - for _, section := range strings.Split(name, ".") { + for section := range strings.SplitSeq(name, ".") { // Check that the data we're using is in the supported format - var m map[string]interface{} + var m map[string]any - m, isMap := result.(map[string]interface{}) + m, isMap := result.(map[string]any) if !isMap { return nil, fmt.Errorf("attribute %v not found", name) @@ -203,7 +203,7 @@ func (a *ItemAttributes) Get(name string) (interface{}, error) { // 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 { +func (a *ItemAttributes) Set(name string, value any) error { // Check to make sure that the pointer is not nil if a == nil { return errors.New("Set called on nil pointer") @@ -409,7 +409,7 @@ func AddDefaultTransforms(customTransforms TransformMap) TransformMap { // // 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) { +func ToAttributesCustom(m map[string]any, sort bool, customTransforms TransformMap) (*ItemAttributes, error) { return toAttributes(m, sort, customTransforms) } @@ -417,16 +417,16 @@ func ToAttributesCustom(m map[string]interface{}, sort bool, customTransforms Tr // 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) { +func ToAttributesSorted(m map[string]any) (*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) { +func ToAttributes(m map[string]any) (*ItemAttributes, error) { return toAttributes(m, false, DefaultTransforms) } -func toAttributes(m map[string]interface{}, sort bool, customTransforms TransformMap) (*ItemAttributes, error) { +func toAttributes(m map[string]any, sort bool, customTransforms TransformMap) (*ItemAttributes, error) { if m == nil { return nil, nil } @@ -457,13 +457,13 @@ func toAttributes(m map[string]interface{}, sort bool, customTransforms Transfor // 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) { +func ToAttributesViaJson(v any) (*ItemAttributes, error) { b, err := json.Marshal(v) if err != nil { return nil, err } - var m map[string]interface{} + var m map[string]any err = json.Unmarshal(b, &m) if err != nil { @@ -475,7 +475,7 @@ func ToAttributesViaJson(v interface{}) (*ItemAttributes, error) { // 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{} +type TransformFunc func(any) any // A map of types to transform functions type TransformMap map[reflect.Type]TransformFunc @@ -483,11 +483,11 @@ 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{} { + reflect.TypeFor[time.Time](): func(i any) any { return i.(time.Time).Format(time.RFC3339Nano) }, // Duration should be in string format - reflect.TypeOf(time.Duration(0)): func(i interface{}) interface{} { + reflect.TypeFor[time.Duration](): func(i any) any { return i.(time.Duration).String() }, } @@ -515,7 +515,7 @@ var DefaultTransforms = TransformMap{ // 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{} { +func sanitizeInterface(i any, sortArrays bool, customTransforms TransformMap) any { if i == nil { return nil } @@ -571,9 +571,9 @@ func sanitizeInterface(i interface{}, sortArrays bool, customTransforms Transfor // conversion on that // returnSlice Returns the array in the format that protobuf can deal with - var returnSlice []interface{} + var returnSlice []any - returnSlice = make([]interface{}, v.Len()) + returnSlice = make([]any, v.Len()) for i := range v.Len() { returnSlice[i] = sanitizeInterface(v.Index(i).Interface(), sortArrays, customTransforms) @@ -585,9 +585,9 @@ func sanitizeInterface(i interface{}, sortArrays bool, customTransforms Transfor return returnSlice case reflect.Map: - var returnMap map[string]interface{} + var returnMap map[string]any - returnMap = make(map[string]interface{}) + returnMap = make(map[string]any) for _, mapKey := range v.MapKeys() { // Convert the key to a string @@ -602,9 +602,9 @@ func sanitizeInterface(i interface{}, sortArrays bool, customTransforms Transfor 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{} + var returnMap map[string]any - returnMap = make(map[string]interface{}) + returnMap = make(map[string]any) // Range over fields n := t.NumField() @@ -629,7 +629,7 @@ func sanitizeInterface(i interface{}, sortArrays bool, customTransforms Transfor } return sanitizeInterface(returnMap, sortArrays, customTransforms) - case reflect.Ptr: + case reflect.Pointer: // Get the zero value for this field zero := reflect.Zero(t) @@ -648,7 +648,7 @@ func sanitizeInterface(i interface{}, sortArrays bool, customTransforms Transfor // Sorts an interface slice by converting each item to a string and sorting // these strings -func sortInterfaceArray(input []interface{}) { +func sortInterfaceArray(input []any) { sort.Slice(input, func(i, j int) bool { return fmt.Sprint(input[i]) < fmt.Sprint(input[j]) }) diff --git a/go/sdp-go/items_test.go b/go/sdp-go/items_test.go index 666f3c69..a85f4ed0 100644 --- a/go/sdp-go/items_test.go +++ b/go/sdp-go/items_test.go @@ -16,7 +16,7 @@ import ( type ToAttributesTest struct { Name string - Input map[string]interface{} + Input map[string]any } type CustomString string @@ -29,24 +29,24 @@ 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"` + Foo string `json:",omitempty"` + Bar string `json:",omitempty"` + Baz string `json:",omitempty"` + Time time.Time Duration time.Duration `json:",omitempty"` } var ToAttributesTests = []ToAttributesTest{ { Name: "Basic strings map", - Input: map[string]interface{}{ + Input: map[string]any{ "firstName": "Dylan", "lastName": "Ratcliffe", }, }, { Name: "Arrays map", - Input: map[string]interface{}{ + Input: map[string]any{ "empty": []string{}, "single-level": []string{ "one", @@ -66,7 +66,7 @@ var ToAttributesTests = []ToAttributesTest{ }, { Name: "Nested strings maps", - Input: map[string]interface{}{ + Input: map[string]any{ "strings map": map[string]string{ "foo": "bar", }, @@ -74,7 +74,7 @@ var ToAttributesTests = []ToAttributesTest{ }, { Name: "Nested integer map", - Input: map[string]interface{}{ + Input: map[string]any{ "numbers map": map[string]int{ "one": 1, "two": 2, @@ -83,7 +83,7 @@ var ToAttributesTests = []ToAttributesTest{ }, { Name: "Nested string-array map", - Input: map[string]interface{}{ + Input: map[string]any{ "arrays map": map[string][]string{ "dogs": { "pug", @@ -94,7 +94,7 @@ var ToAttributesTests = []ToAttributesTest{ }, { Name: "Nested non-string keys map", - Input: map[string]interface{}{ + Input: map[string]any{ "non-string keys": map[int]string{ 1: "one", 2: "two", @@ -104,21 +104,21 @@ var ToAttributesTests = []ToAttributesTest{ }, { Name: "Composite types", - Input: map[string]interface{}{ + Input: map[string]any{ "underlying string": Dylan, "underlying bool": Bool1, }, }, { Name: "Pointers", - Input: map[string]interface{}{ + Input: map[string]any{ "pointer bool": &Bool1, "pointer string": &Dylan, }, }, { Name: "structs", - Input: map[string]interface{}{ + Input: map[string]any{ "named struct": CustomStruct{ Foo: "foo", Bar: "bar", @@ -134,7 +134,7 @@ var ToAttributesTests = []ToAttributesTest{ }, { Name: "Zero-value structs", - Input: map[string]interface{}{ + Input: map[string]any{ "something": CustomStruct{ Foo: "yes", Time: time.Now(), @@ -180,8 +180,8 @@ func TestToAttributes(t *testing.T) { t.Fatal(err) } - var input map[string]interface{} - var output map[string]interface{} + var input map[string]any + var output map[string]any err = json.Unmarshal(inputBytes, &input) @@ -208,7 +208,7 @@ func TestToAttributes(t *testing.T) { } func TestDefaultTransformMap(t *testing.T) { - input := map[string]interface{}{ + input := map[string]any{ // Use a duration "hour": 1 * time.Hour, } @@ -236,8 +236,8 @@ func TestCustomTransforms(t *testing.T) { Value string } - data := map[string]interface{}{ - "user": map[string]interface{}{ + data := map[string]any{ + "user": map[string]any{ "name": "Hunter", "password": Secret{ Value: "hunter2", @@ -246,7 +246,7 @@ func TestCustomTransforms(t *testing.T) { } attributes, err := ToAttributesCustom(data, true, TransformMap{ - reflect.TypeOf(Secret{}): func(i interface{}) interface{} { + reflect.TypeFor[Secret](): func(i any) any { // Remove it return "REDACTED" }, @@ -262,7 +262,7 @@ func TestCustomTransforms(t *testing.T) { t.Fatal(err) } - userMap, ok := user.(map[string]interface{}) + userMap, ok := user.(map[string]any) if !ok { t.Fatalf("Expected user to be a map, got %T", user) @@ -280,7 +280,7 @@ func TestCustomTransforms(t *testing.T) { Bar string } - data := map[string]interface{}{ + data := map[string]any{ "something": Something{ Foo: "foo", Bar: "bar", @@ -288,7 +288,7 @@ func TestCustomTransforms(t *testing.T) { } attributes, err := ToAttributesCustom(data, true, TransformMap{ - reflect.TypeOf(Something{}): func(i interface{}) interface{} { + reflect.TypeFor[Something](): func(i any) any { something := i.(Something) return map[string]string{ @@ -308,7 +308,7 @@ func TestCustomTransforms(t *testing.T) { t.Fatal(err) } - somethingMap, ok := something.(map[string]interface{}) + somethingMap, ok := something.(map[string]any) if !ok { t.Fatalf("Expected something to be a map, got %T", something) @@ -328,7 +328,7 @@ func TestCustomTransforms(t *testing.T) { Bar string } - data := map[string]interface{}{ + data := map[string]any{ "something": Something{ Foo: "foo", Bar: "bar", @@ -337,7 +337,7 @@ func TestCustomTransforms(t *testing.T) { } _, err := ToAttributesCustom(data, true, TransformMap{ - reflect.TypeOf(Something{}): func(i interface{}) interface{} { + reflect.TypeFor[Something](): func(i any) any { return nil }, }) @@ -349,7 +349,7 @@ func TestCustomTransforms(t *testing.T) { } func TestCopy(t *testing.T) { - exampleAttributes, err := ToAttributes(map[string]interface{}{ + exampleAttributes, err := ToAttributes(map[string]any{ "name": "Dylan", "friend": "Mike", "age": 27, @@ -472,8 +472,8 @@ func AssertItemsEqual(itemA *Item, itemB *Item, t *testing.T) { t.Error("UniqueAttribute did not match") } - var nameA interface{} - var nameB interface{} + var nameA any + var nameB any var err error nameA, err = itemA.GetAttributes().Get("name") @@ -643,9 +643,9 @@ func TestToAttributesViaJson(t *testing.T) { } func TestAttributesGet(t *testing.T) { - mapData := map[string]interface{}{ + mapData := map[string]any{ "foo": "bar", - "nest": map[string]interface{}{ + "nest": map[string]any{ "nest2": map[string]string{ "nest3": "nestValue", }, @@ -668,9 +668,9 @@ func TestAttributesGet(t *testing.T) { } func TestAttributesSet(t *testing.T) { - mapData := map[string]interface{}{ + mapData := map[string]any{ "foo": "bar", - "nest": map[string]interface{}{ + "nest": map[string]any{ "nest2": map[string]string{ "nest3": "nestValue", }, diff --git a/go/sdp-go/link_extract.go b/go/sdp-go/link_extract.go index 9f03b99f..8041e61b 100644 --- a/go/sdp-go/link_extract.go +++ b/go/sdp-go/link_extract.go @@ -28,8 +28,8 @@ func ExtractLinksFromAttributes(attributes *ItemAttributes) []*LinkedItemQuery { // 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{}{ +func ExtractLinksFrom(anything any) ([]*LinkedItemQuery, error) { + attributes, err := ToAttributes(map[string]any{ "": anything, }) if err != nil { diff --git a/go/sdp-go/link_extract_test.go b/go/sdp-go/link_extract_test.go index 0314b0f4..4b2af566 100644 --- a/go/sdp-go/link_extract_test.go +++ b/go/sdp-go/link_extract_test.go @@ -7,7 +7,7 @@ import ( ) // Create a very large set of attributes for the benchmark -func createTestData() (*ItemAttributes, interface{}) { +func createTestData() (*ItemAttributes, any) { yamlString := `--- creationTimestamp: 2024-07-09T11:16:31Z data: @@ -414,7 +414,7 @@ taskArn: arn:aws:ecs:eu-west-2:123456789:task/example-tfc/ded4f8eebe4144ddb9a93a version: 5 ` - mapData := make(map[string]interface{}) + mapData := make(map[string]any) _ = yaml.Unmarshal([]byte(yamlString), &mapData) attrs, _ := ToAttributes(mapData) @@ -616,7 +616,7 @@ func TestExtractLinksFromAttributes(t *testing.T) { func TestExtractLinksFrom(t *testing.T) { tests := []struct { Name string - Object interface{} + Object any ExpectedQueries []string }{ { @@ -677,8 +677,8 @@ func TestExtractLinksFrom(t *testing.T) { func TestExtractLinksFromConfigMapData(t *testing.T) { // Test ConfigMap data with S3 bucket ARN - configMapData := map[string]interface{}{ - "data": map[string]interface{}{ + configMapData := map[string]any{ + "data": map[string]any{ "S3_BUCKET_ARN": "arn:aws:s3:::example-bucket-name", "S3_BUCKET_NAME": "example-bucket-name", }, @@ -761,7 +761,7 @@ func TestS3BucketARNTypeDetection(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - queries, err := ExtractLinksFrom(map[string]interface{}{ + queries, err := ExtractLinksFrom(map[string]any{ "arn": tt.arn, }) if err != nil { diff --git a/go/sdp-go/progress_test.go b/go/sdp-go/progress_test.go index 02c8adfd..e0d9a533 100644 --- a/go/sdp-go/progress_test.go +++ b/go/sdp-go/progress_test.go @@ -495,9 +495,7 @@ func TestQueryProgressParallel(t *testing.T) { var wg sync.WaitGroup for i := 0; i != 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { // Test the initial response sq.handleQueryResponse(ctx, &QueryResponse{ ResponseType: &QueryResponse_Response{ @@ -509,7 +507,7 @@ func TestQueryProgressParallel(t *testing.T) { }, }, }) - }() + }) } wg.Wait() @@ -637,8 +635,7 @@ func TestRogueResponder(t *testing.T) { // 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() + tickerCtx := t.Context() defer ticker.Stop() go func() { diff --git a/go/sdp-go/proto_clone_test.go b/go/sdp-go/proto_clone_test.go index 863c8f7d..546104e1 100644 --- a/go/sdp-go/proto_clone_test.go +++ b/go/sdp-go/proto_clone_test.go @@ -79,7 +79,7 @@ func TestProtoCloneReplacesCustomCopy(t *testing.T) { } // Add attributes - attrs, err := ToAttributes(map[string]interface{}{ + attrs, err := ToAttributes(map[string]any{ "name": "test-item", "port": 8080, }) diff --git a/go/sdp-go/sdpws/client.go b/go/sdp-go/sdpws/client.go index 7c7f62cf..f862824a 100644 --- a/go/sdp-go/sdpws/client.go +++ b/go/sdp-go/sdpws/client.go @@ -64,9 +64,9 @@ type Client struct { // 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 + receiveCtx context.Context + receiveCancel context.CancelFunc + receiveDone sync.WaitGroup } // Dial connects to the given URL and returns a new Client. Pass nil as handler @@ -123,11 +123,9 @@ func dialImpl(ctx context.Context, u string, httpClient *http.Client, handler Ga // 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.receiveDone.Go(func() { c.receive(c.receiveCtx) - }() + }) return c, nil } diff --git a/go/sdp-go/sdpws/client_test.go b/go/sdp-go/sdpws/client_test.go index 57fdd5dd..c3e3a460 100644 --- a/go/sdp-go/sdpws/client_test.go +++ b/go/sdp-go/sdpws/client_test.go @@ -7,6 +7,7 @@ import ( "net/http" "net/http/httptest" "os" + "slices" "sync" "testing" "time" @@ -20,12 +21,7 @@ import ( // 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 + return slices.Contains(slice, item) } // TestServer is a test server for the websocket client. Note that this can only @@ -643,19 +639,15 @@ func TestClient(t *testing.T) { var wg sync.WaitGroup results := make([]result, 2) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { items, err := c.QueryOne(ctx, query1) results[0] = result{items: items, err: err} - }() + }) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { items, err := c.QueryOne(ctx, query2) results[1] = result{items: items, err: err} - }() + }) wg.Wait() @@ -797,19 +789,15 @@ func TestClient(t *testing.T) { resultsA := make([]result, 1) resultsB := make([]result, 1) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { items, err := c.QueryOne(ctx, queryA) resultsA[0] = result{items: items, err: err} - }() + }) - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { items, err := c.QueryOne(ctx, queryB) resultsB[0] = result{items: items, err: err} - }() + }) wg.Wait() @@ -914,9 +902,7 @@ func TestRaceConditionOnClose(t *testing.T) { // 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() + c.receiveDone.Go(func() { // Simulate receive() processing messages and calling postRequestChan // It will be cancelled by Close() and should stop before channels are closed for i := range 1000 { @@ -939,19 +925,17 @@ func TestRaceConditionOnClose(t *testing.T) { }() 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() + wg.Go(func() { // 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() diff --git a/go/sdp-go/test_utils.go b/go/sdp-go/test_utils.go index 0ca45d10..fbb4c1ec 100644 --- a/go/sdp-go/test_utils.go +++ b/go/sdp-go/test_utils.go @@ -15,7 +15,7 @@ import ( type ResponseMessage struct { Subject string - V interface{} + V any } // TestConnection Used to mock a NATS connection for testing @@ -140,7 +140,7 @@ func (r *TestConnection) subjectToRegexp(subject string) *regexp.Regexp { func (t *TestConnection) RequestMsg(ctx context.Context, msg *nats.Msg) (*nats.Msg, error) { replySubject := randSeq(10) msg.Reply = replySubject - replies := make(chan interface{}, 128) + replies := make(chan any, 128) // Subscribe to the reply subject _, err := t.Subscribe(replySubject, func(msg *nats.Msg) { diff --git a/go/sdpcache/bolt_cache.go b/go/sdpcache/bolt_cache.go index 9d740310..5034ed06 100644 --- a/go/sdpcache/bolt_cache.go +++ b/go/sdpcache/bolt_cache.go @@ -148,7 +148,7 @@ func parseExpiryKey(key []byte) (time.Time, SSTHash, []byte, error) { } expiryNanoUint := binary.BigEndian.Uint64(key[0:8]) - expiryNano := int64(expiryNanoUint) + expiryNano := int64(expiryNanoUint) //nolint:gosec // G115 (overflow): guarded by underflow check on lines 153-155 that clamps to zero // Check for overflow when converting uint64 to int64 if expiryNano < 0 && expiryNanoUint > 0 { expiryNano = 0 @@ -157,13 +157,13 @@ func parseExpiryKey(key []byte) (time.Time, SSTHash, []byte, error) { // Find the separators rest := key[9:] // skip the first separator - sepIdx := bytes.IndexByte(rest, '|') - if sepIdx < 0 { + before, after, ok := bytes.Cut(rest, []byte{'|'}) + if !ok { return time.Time{}, "", nil, errors.New("invalid expiry key format") } - sstHash := SSTHash(rest[:sepIdx]) - entryKey := rest[sepIdx+1:] + sstHash := SSTHash(before) + entryKey := after return expiry, sstHash, entryKey, nil } @@ -229,7 +229,7 @@ func WithCompactThreshold(bytes int64) BoltCacheOption { func NewBoltCache(path string, opts ...BoltCacheOption) (*BoltCache, error) { // Ensure the directory exists dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0o755); err != nil { + if err := os.MkdirAll(dir, 0o755); err != nil { //nolint:gosec // G301 (path traversal): path comes from application config (NewBoltCache callers), not user HTTP input return nil, fmt.Errorf("failed to create directory: %w", err) } @@ -294,7 +294,7 @@ func (c *BoltCache) loadDeletedBytes() error { data := meta.Get(deletedBytesKey) if len(data) == 8 { deletedBytesUint := binary.BigEndian.Uint64(data) - deletedBytes := int64(deletedBytesUint) + deletedBytes := int64(deletedBytesUint) //nolint:gosec // G115 (overflow): guarded by underflow check on lines 299-301 that clamps to zero // Check for overflow when converting uint64 to int64 if deletedBytes < 0 && deletedBytesUint > 0 { deletedBytes = 0 @@ -402,12 +402,12 @@ func (c *BoltCache) CloseAndDestroy() error { // 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) } diff --git a/go/sdpcache/cache.go b/go/sdpcache/cache.go index d6089394..9655c38f 100644 --- a/go/sdpcache/cache.go +++ b/go/sdpcache/cache.go @@ -341,7 +341,7 @@ func NewCache(ctx context.Context) Cache { if err != nil { sentry.CaptureException(err) log.WithError(err).Error("Failed to create BoltCache, using memory cache instead") - _ = os.Remove(tmpFile.Name()) + _ = os.Remove(tmpFile.Name()) //nolint:gosec // G304 (path traversal): path generated by os.CreateTemp, not from user input cache := NewMemoryCache() cache.StartPurger(ctx) return cache diff --git a/go/sdpcache/cache_stuck_test.go b/go/sdpcache/cache_stuck_test.go index b5118e18..38681940 100644 --- a/go/sdpcache/cache_stuck_test.go +++ b/go/sdpcache/cache_stuck_test.go @@ -37,9 +37,7 @@ func TestListErrorWithProperCleanup(t *testing.T) { // 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() + wg.Go(func() { <-startBarrier hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) @@ -60,12 +58,10 @@ func TestListErrorWithProperCleanup(t *testing.T) { } 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() + wg.Go(func() { <-startBarrier // Small delay to ensure first goroutine starts first @@ -87,7 +83,7 @@ func TestListErrorWithProperCleanup(t *testing.T) { t.Error("second goroutine: expected cached error") } t.Logf("Second goroutine: got cached error after %v", secondCallDuration) - }() + }) // Release all goroutines close(startBarrier) @@ -128,9 +124,7 @@ func TestListErrorWithProperDone(t *testing.T) { // First goroutine: Gets cache miss, simulates work that errors, // and PROPERLY calls the done function - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-startBarrier hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) @@ -147,12 +141,10 @@ func TestListErrorWithProperDone(t *testing.T) { // 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() + wg.Go(func() { <-startBarrier // Small delay to ensure first goroutine starts first @@ -168,7 +160,7 @@ func TestListErrorWithProperDone(t *testing.T) { } t.Logf("Second goroutine: got cache miss after %v", secondCallDuration) - }() + }) // Release all goroutines close(startBarrier) @@ -220,9 +212,7 @@ func TestListErrorWithStoreError(t *testing.T) { // First goroutine: Gets cache miss, simulates work that errors, // and PROPERLY calls StoreError - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-startBarrier hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) @@ -239,12 +229,10 @@ func TestListErrorWithStoreError(t *testing.T) { // 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() + wg.Go(func() { <-startBarrier // Small delay to ensure first goroutine starts first @@ -262,7 +250,7 @@ func TestListErrorWithStoreError(t *testing.T) { } t.Logf("Second goroutine: got result after %v", secondCallDuration) - }() + }) // Release all goroutines close(startBarrier) @@ -314,9 +302,7 @@ func TestListReturnsEmptyButNoStore(t *testing.T) { var secondCallDuration time.Duration // First goroutine: LIST returns 0 items, completes without storing - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-startBarrier hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) @@ -340,12 +326,10 @@ func TestListReturnsEmptyButNoStore(t *testing.T) { } t.Log("First goroutine: completed work but stored nothing") - }() + }) // Second goroutine: Should get cache miss - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-startBarrier // Small delay to ensure first goroutine starts first @@ -357,7 +341,7 @@ func TestListReturnsEmptyButNoStore(t *testing.T) { secondCallDuration = time.Since(start) t.Logf("Second goroutine: hit=%v, duration=%v", secondCallHit, secondCallDuration) - }() + }) // Release all goroutines close(startBarrier) diff --git a/go/sdpcache/cache_test.go b/go/sdpcache/cache_test.go index 8003557f..c6c2413b 100644 --- a/go/sdpcache/cache_test.go +++ b/go/sdpcache/cache_test.go @@ -37,16 +37,16 @@ func cacheImplementations(tb testing.TB) []struct { 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 - }}, + {"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 + }}, } } @@ -862,7 +862,7 @@ func TestMultipleItemsSameSST(t *testing.T) { uav := fmt.Sprintf("item%d", i) // Set the item's unique attribute value to match the CacheKey - attrs := make(map[string]interface{}) + attrs := make(map[string]any) if item.GetAttributes() != nil && item.GetAttributes().GetAttrStruct() != nil { for k, v := range item.GetAttributes().GetAttrStruct().GetFields() { attrs[k] = v @@ -1030,22 +1030,18 @@ func TestMemoryCacheConcurrent(t *testing.T) { numParallel := 1_000 for range numParallel { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { // 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() + wg.Go(func() { cache.Delete(ck) - }() - }() + }) + }) } wg.Wait() @@ -1145,9 +1141,7 @@ func TestMemoryCacheLookupDeduplicationCompleteWithoutStore(t *testing.T) { numWaiters := 3 // First goroutine: starts work and completes without storing anything - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-startBarrier hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) @@ -1162,13 +1156,11 @@ func TestMemoryCacheLookupDeduplicationCompleteWithoutStore(t *testing.T) { // 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() + wg.Go(func() { <-startBarrier time.Sleep(10 * time.Millisecond) @@ -1179,7 +1171,7 @@ func TestMemoryCacheLookupDeduplicationCompleteWithoutStore(t *testing.T) { waiterMu.Lock() waiterHits = append(waiterHits, hit) waiterMu.Unlock() - }() + }) } close(startBarrier) @@ -1484,13 +1476,11 @@ func TestBoltCacheConcurrentCloseAndDestroy(t *testing.T) { // Launch concurrent read/write operations for range numOperations { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { 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 @@ -1498,14 +1488,12 @@ func TestBoltCacheConcurrentCloseAndDestroy(t *testing.T) { // Close and destroy while operations are in flight // The compaction lock should serialize this properly - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { err := cache.CloseAndDestroy() if err != nil { t.Logf("CloseAndDestroy returned error: %v", err) } - }() + }) // Wait for all operations to complete wg.Wait() @@ -1753,9 +1741,7 @@ func TestBoltCacheLookupDeduplicationTimeout(t *testing.T) { startBarrier := make(chan struct{}) // First goroutine: does the work but takes a long time - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-startBarrier hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) @@ -1773,13 +1759,11 @@ func TestBoltCacheLookupDeduplicationTimeout(t *testing.T) { 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() + wg.Go(func() { <-startBarrier // Small delay to ensure first goroutine starts first @@ -1792,7 +1776,7 @@ func TestBoltCacheLookupDeduplicationTimeout(t *testing.T) { hit, _, _, _, done := cache.Lookup(shortCtx, sst.SourceName, method, sst.Scope, sst.Type, query, false) defer done() secondHit = hit - }() + }) // Release all goroutines close(startBarrier) @@ -1840,9 +1824,7 @@ func TestBoltCacheLookupDeduplicationError(t *testing.T) { numWaiters := 5 // First goroutine: does the work and stores an error - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-startBarrier hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) @@ -1857,13 +1839,11 @@ func TestBoltCacheLookupDeduplicationError(t *testing.T) { // 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() + wg.Go(func() { <-startBarrier // Small delay to ensure first goroutine starts first @@ -1877,7 +1857,7 @@ func TestBoltCacheLookupDeduplicationError(t *testing.T) { waiterErrors = append(waiterErrors, qErr) } waiterMu.Unlock() - }() + }) } // Release all goroutines @@ -1924,9 +1904,7 @@ func TestBoltCacheLookupDeduplicationCancel(t *testing.T) { numWaiters := 3 // First goroutine: starts work but then calls done() without storing anything - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { <-startBarrier hit, _, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) @@ -1939,13 +1917,11 @@ func TestBoltCacheLookupDeduplicationCancel(t *testing.T) { // 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() + wg.Go(func() { <-startBarrier // Small delay to ensure first goroutine starts first @@ -1957,7 +1933,7 @@ func TestBoltCacheLookupDeduplicationCancel(t *testing.T) { waiterMu.Lock() waiterHits = append(waiterHits, hit) waiterMu.Unlock() - }() + }) } // Release all goroutines @@ -2002,9 +1978,7 @@ func TestBoltCacheLookupDeduplicationCompleteWithoutStore(t *testing.T) { // 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() + wg.Go(func() { <-startBarrier hit, ck, _, _, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, query, false) @@ -2020,13 +1994,11 @@ func TestBoltCacheLookupDeduplicationCompleteWithoutStore(t *testing.T) { // 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() + wg.Go(func() { <-startBarrier // Small delay to ensure first goroutine starts first @@ -2038,7 +2010,7 @@ func TestBoltCacheLookupDeduplicationCompleteWithoutStore(t *testing.T) { waiterMu.Lock() waiterHits = append(waiterHits, hit) waiterMu.Unlock() - }() + }) } // Release all goroutines @@ -2105,11 +2077,9 @@ func TestPendingWorkUnit(t *testing.T) { var wg sync.WaitGroup var waitOk bool - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { waitOk = pw.Wait(ctx, entry) - }() + }) // Give waiter time to start waiting time.Sleep(10 * time.Millisecond) @@ -2135,11 +2105,9 @@ func TestPendingWorkUnit(t *testing.T) { var wg sync.WaitGroup var waitOk bool - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { waitOk = pw.Wait(ctx, entry) - }() + }) // Give waiter time to start waiting time.Sleep(10 * time.Millisecond) diff --git a/go/sdpcache/item_generator_test.go b/go/sdpcache/item_generator_test.go index 602a5c8b..2729a5cd 100644 --- a/go/sdpcache/item_generator_test.go +++ b/go/sdpcache/item_generator_test.go @@ -44,7 +44,7 @@ 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{}) + attrs := make(map[string]any) name := randSeq(rand.Intn(MaxAttributeValueLength)) typ := Types[rand.Intn(len(Types))] diff --git a/go/tracing/deferlog.go b/go/tracing/deferlog.go index b2fb04c7..b1717551 100644 --- a/go/tracing/deferlog.go +++ b/go/tracing/deferlog.go @@ -56,7 +56,7 @@ func LogRecoverToExit(ctx context.Context, loc string) { os.Exit(1) } -func HandleError(ctx context.Context, loc string, err interface{}, stack string) { +func HandleError(ctx context.Context, loc string, err any, stack string) { msg := fmt.Sprintf("unhandled panic in %v, exiting: %v", loc, err) hub := sentry.CurrentHub() diff --git a/k8s-source/adapters/generic_source.go b/k8s-source/adapters/generic_source.go index 6f331f59..9a139961 100644 --- a/k8s-source/adapters/generic_source.go +++ b/k8s-source/adapters/generic_source.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "time" "github.com/overmindtech/cli/go/sdp-go" @@ -312,13 +313,7 @@ var ignoredMetadataFields = []string{ } func ignored(key string) bool { - for _, ignoredKey := range ignoredMetadataFields { - if key == ignoredKey { - return true - } - } - - return false + return slices.Contains(ignoredMetadataFields, key) } // resourcesToItems Converts a slice of resources to a slice of items @@ -360,7 +355,7 @@ func (s *KubeTypeAdapter[Resource, ResourceList]) resourceToItem(resource Resour // Promote the metadata to the top level if metadata, err := attributes.Get("metadata"); err == nil { // Cast to a type we can iterate over - if metadataMap, ok := metadata.(map[string]interface{}); ok { + if metadataMap, ok := metadata.(map[string]any); ok { for key, value := range metadataMap { // Check that the key isn't in the ignored list if !ignored(key) { diff --git a/knowledge/discover.go b/knowledge/discover.go index f675d71c..d0468d65 100644 --- a/knowledge/discover.go +++ b/knowledge/discover.go @@ -173,14 +173,14 @@ func processFile(path, relPath string) (*KnowledgeFile, *Warning) { 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 { @@ -239,7 +239,7 @@ func parseFrontmatter(content string) (string, string, string, error) { // 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" @@ -247,16 +247,16 @@ func parseFrontmatter(content string) (string, string, string, error) { 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 { @@ -294,10 +294,7 @@ func parseFrontmatter(content string) (string, string, string, error) { } // Extract body using the correct offset for the delimiter type found - bodyStartIdx := startIdx + endIdx + closingDelimLen - if bodyStartIdx > len(content) { - bodyStartIdx = len(content) - } + bodyStartIdx := min(startIdx+endIdx+closingDelimLen, len(content)) body := strings.TrimLeft(content[bodyStartIdx:], "\n\r") // Trim whitespace from name and description as per validation @@ -347,12 +344,12 @@ func DiscoverAndConvert(ctx context.Context, knowledgeDir string) []*sdp.Knowled } 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 { @@ -363,11 +360,11 @@ func DiscoverAndConvert(ctx context.Context, knowledgeDir string) []*sdp.Knowled 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/sources/azure/integration-tests/authorization-role-assignment_test.go b/sources/azure/integration-tests/authorization-role-assignment_test.go index b4548094..300d5588 100644 --- a/sources/azure/integration-tests/authorization-role-assignment_test.go +++ b/sources/azure/integration-tests/authorization-role-assignment_test.go @@ -15,7 +15,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" "github.com/google/uuid" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -378,8 +377,8 @@ func createRoleAssignment(ctx context.Context, client *armauthorization.RoleAssi parameters := armauthorization.RoleAssignmentCreateParameters{ Properties: &armauthorization.RoleAssignmentProperties{ - PrincipalID: ptr.To(principalID), - RoleDefinitionID: ptr.To(roleDefinitionID), + PrincipalID: new(principalID), + RoleDefinitionID: new(roleDefinitionID), }, } diff --git a/sources/azure/integration-tests/batch-batch-accounts_test.go b/sources/azure/integration-tests/batch-batch-accounts_test.go index 4aea0a93..d3836ebe 100644 --- a/sources/azure/integration-tests/batch-batch-accounts_test.go +++ b/sources/azure/integration-tests/batch-batch-accounts_test.go @@ -16,7 +16,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -320,16 +319,16 @@ func createBatchAccount(ctx context.Context, client *armbatch.AccountClient, res // Create the batch account poller, err := client.BeginCreate(ctx, resourceGroupName, accountName, armbatch.AccountCreateParameters{ - Location: ptr.To(location), + Location: new(location), Properties: &armbatch.AccountCreateProperties{ AutoStorage: &armbatch.AutoStorageBaseProperties{ - StorageAccountID: ptr.To(storageAccountID), + StorageAccountID: new(storageAccountID), }, - PoolAllocationMode: ptr.To(armbatch.PoolAllocationModeBatchService), + PoolAllocationMode: new(armbatch.PoolAllocationModeBatchService), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("batch-account"), + "purpose": new("overmind-integration-tests"), + "test": new("batch-account"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/compute-availability-set_test.go b/sources/azure/integration-tests/compute-availability-set_test.go index fc7cde62..76f6f232 100644 --- a/sources/azure/integration-tests/compute-availability-set_test.go +++ b/sources/azure/integration-tests/compute-availability-set_test.go @@ -14,7 +14,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "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" @@ -379,16 +378,16 @@ func createAvailabilitySet(ctx context.Context, client *armcompute.AvailabilityS // Create the availability set resp, err := client.CreateOrUpdate(ctx, resourceGroupName, avSetName, armcompute.AvailabilitySet{ - Location: ptr.To(location), + Location: new(location), Properties: &armcompute.AvailabilitySetProperties{ - PlatformFaultDomainCount: ptr.To[int32](2), - PlatformUpdateDomainCount: ptr.To[int32](2), + PlatformFaultDomainCount: new(int32(2)), + PlatformUpdateDomainCount: new(int32(2)), ProximityPlacementGroup: nil, // Optional - not setting for this test VirtualMachines: nil, // Will be populated when VMs are added }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-availability-set"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-availability-set"), }, }, nil) if err != nil { @@ -473,22 +472,22 @@ func createVirtualNetworkForAVSet(ctx context.Context, client *armnetwork.Virtua // Create the VNet poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vnetName, armnetwork.VirtualNetwork{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.VirtualNetworkPropertiesFormat{ AddressSpace: &armnetwork.AddressSpace{ - AddressPrefixes: []*string{ptr.To("10.2.0.0/16")}, + AddressPrefixes: []*string{new("10.2.0.0/16")}, }, Subnets: []*armnetwork.Subnet{ { - Name: ptr.To(integrationTestSubnetForAVSetName), + Name: new(integrationTestSubnetForAVSetName), Properties: &armnetwork.SubnetPropertiesFormat{ - AddressPrefix: ptr.To("10.2.0.0/24"), + AddressPrefix: new("10.2.0.0/24"), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), + "purpose": new("overmind-integration-tests"), }, }, nil) if err != nil { @@ -515,22 +514,22 @@ func createNetworkInterfaceForAVSet(ctx context.Context, client *armnetwork.Inte // Create the NIC poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, nicName, armnetwork.Interface{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.InterfacePropertiesFormat{ IPConfigurations: []*armnetwork.InterfaceIPConfiguration{ { - Name: ptr.To("ipconfig1"), + Name: new("ipconfig1"), Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{ Subnet: &armnetwork.Subnet{ - ID: ptr.To(subnetID), + ID: new(subnetID), }, - PrivateIPAllocationMethod: ptr.To(armnetwork.IPAllocationMethodDynamic), + PrivateIPAllocationMethod: new(armnetwork.IPAllocationMethodDynamic), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), + "purpose": new("overmind-integration-tests"), }, }, nil) if err != nil { @@ -567,54 +566,54 @@ func createVirtualMachineWithAvailabilitySet(ctx context.Context, client *armcom // Create the VM poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vmName, armcompute.VirtualMachine{ - Location: ptr.To(location), + Location: new(location), Properties: &armcompute.VirtualMachineProperties{ HardwareProfile: &armcompute.HardwareProfile{ // Use Standard_D2ps_v5 - ARM-based VM with good availability in westus2 - VMSize: ptr.To(armcompute.VirtualMachineSizeTypes("Standard_D2ps_v5")), + VMSize: new(armcompute.VirtualMachineSizeTypes("Standard_D2ps_v5")), }, StorageProfile: &armcompute.StorageProfile{ ImageReference: &armcompute.ImageReference{ - Publisher: ptr.To("Canonical"), - Offer: ptr.To("0001-com-ubuntu-server-jammy"), - SKU: ptr.To("22_04-lts-arm64"), // ARM64 image for ARM-based VM - Version: ptr.To("latest"), + Publisher: new("Canonical"), + Offer: new("0001-com-ubuntu-server-jammy"), + SKU: new("22_04-lts-arm64"), // ARM64 image for ARM-based VM + Version: new("latest"), }, OSDisk: &armcompute.OSDisk{ - Name: ptr.To(fmt.Sprintf("%s-osdisk", vmName)), - CreateOption: ptr.To(armcompute.DiskCreateOptionTypesFromImage), + Name: new(fmt.Sprintf("%s-osdisk", vmName)), + CreateOption: new(armcompute.DiskCreateOptionTypesFromImage), ManagedDisk: &armcompute.ManagedDiskParameters{ - StorageAccountType: ptr.To(armcompute.StorageAccountTypesStandardLRS), + StorageAccountType: new(armcompute.StorageAccountTypesStandardLRS), }, - DeleteOption: ptr.To(armcompute.DiskDeleteOptionTypesDelete), + DeleteOption: new(armcompute.DiskDeleteOptionTypesDelete), }, }, OSProfile: &armcompute.OSProfile{ - ComputerName: ptr.To(vmName), - AdminUsername: ptr.To("azureuser"), + ComputerName: new(vmName), + AdminUsername: new("azureuser"), // Use password authentication for integration tests (simpler than SSH keys) - AdminPassword: ptr.To("OvmIntegTest2024!"), + AdminPassword: new("OvmIntegTest2024!"), LinuxConfiguration: &armcompute.LinuxConfiguration{ - DisablePasswordAuthentication: ptr.To(false), + DisablePasswordAuthentication: new(false), }, }, NetworkProfile: &armcompute.NetworkProfile{ NetworkInterfaces: []*armcompute.NetworkInterfaceReference{ { - ID: ptr.To(nicID), + ID: new(nicID), Properties: &armcompute.NetworkInterfaceReferenceProperties{ - Primary: ptr.To(true), + Primary: new(true), }, }, }, }, AvailabilitySet: &armcompute.SubResource{ - ID: ptr.To(availabilitySetID), + ID: new(availabilitySetID), }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-availability-set"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-availability-set"), }, }, nil) if err != nil { 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 9509e715..cd48cf18 100644 --- a/sources/azure/integration-tests/compute-capacity-reservation-group_test.go +++ b/sources/azure/integration-tests/compute-capacity-reservation-group_test.go @@ -12,7 +12,6 @@ import ( "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" @@ -247,10 +246,10 @@ func createCapacityReservationGroup(ctx context.Context, client *armcompute.Capa } _, err = client.CreateOrUpdate(ctx, resourceGroupName, groupName, armcompute.CapacityReservationGroup{ - Location: ptr.To(location), + Location: new(location), Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-capacity-reservation-group"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-capacity-reservation-group"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/compute-dedicated-host-group_test.go b/sources/azure/integration-tests/compute-dedicated-host-group_test.go index 89e1d475..6bdd96e7 100644 --- a/sources/azure/integration-tests/compute-dedicated-host-group_test.go +++ b/sources/azure/integration-tests/compute-dedicated-host-group_test.go @@ -12,7 +12,6 @@ import ( "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" @@ -247,13 +246,13 @@ func createDedicatedHostGroup(ctx context.Context, client *armcompute.DedicatedH } _, err = client.CreateOrUpdate(ctx, resourceGroupName, hostGroupName, armcompute.DedicatedHostGroup{ - Location: ptr.To(location), + Location: new(location), Properties: &armcompute.DedicatedHostGroupProperties{ - PlatformFaultDomainCount: ptr.To[int32](1), + PlatformFaultDomainCount: new(int32(1)), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-dedicated-host-group"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-dedicated-host-group"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/compute-disk-access_test.go b/sources/azure/integration-tests/compute-disk-access_test.go index bea69ff7..32550fa2 100644 --- a/sources/azure/integration-tests/compute-disk-access_test.go +++ b/sources/azure/integration-tests/compute-disk-access_test.go @@ -13,7 +13,6 @@ import ( "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" @@ -261,10 +260,10 @@ func createDiskAccess(ctx context.Context, client *armcompute.DiskAccessesClient } poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, diskAccessName, armcompute.DiskAccess{ - Location: ptr.To(location), + Location: new(location), Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-disk-access"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-disk-access"), }, }, nil) if err != 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 4278e7b6..e86b55a2 100644 --- a/sources/azure/integration-tests/compute-disk-encryption-set_test.go +++ b/sources/azure/integration-tests/compute-disk-encryption-set_test.go @@ -15,7 +15,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -358,26 +357,26 @@ func createDiskEncryptionSet(ctx context.Context, client *armcompute.DiskEncrypt // New DES creation. des := armcompute.DiskEncryptionSet{ - Location: ptr.To(location), + Location: new(location), Identity: &armcompute.EncryptionSetIdentity{ - Type: ptr.To(armcompute.DiskEncryptionSetIdentityTypeUserAssigned), + Type: new(armcompute.DiskEncryptionSetIdentityTypeUserAssigned), UserAssignedIdentities: map[string]*armcompute.UserAssignedIdentitiesValue{ userAssignedIdentityResourceID: &armcompute.UserAssignedIdentitiesValue{}, }, }, Properties: &armcompute.EncryptionSetProperties{ - EncryptionType: ptr.To(armcompute.DiskEncryptionSetTypeEncryptionAtRestWithCustomerKey), + EncryptionType: new(armcompute.DiskEncryptionSetTypeEncryptionAtRestWithCustomerKey), ActiveKey: &armcompute.KeyForDiskEncryptionSet{ - KeyURL: ptr.To(keyURL), + KeyURL: new(keyURL), SourceVault: &armcompute.SourceVault{ - ID: ptr.To(vaultID), + ID: new(vaultID), }, }, - RotationToLatestKeyVersionEnabled: ptr.To(false), + RotationToLatestKeyVersionEnabled: new(false), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-disk-encryption-set"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-disk-encryption-set"), }, } diff --git a/sources/azure/integration-tests/compute-disk_test.go b/sources/azure/integration-tests/compute-disk_test.go index bd47dad0..81049955 100644 --- a/sources/azure/integration-tests/compute-disk_test.go +++ b/sources/azure/integration-tests/compute-disk_test.go @@ -13,7 +13,6 @@ import ( "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" @@ -289,19 +288,19 @@ func createDisk(ctx context.Context, client *armcompute.DisksClient, resourceGro // Create an empty disk (DiskCreateOptionEmpty) // This is the simplest type of disk to create for testing poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, diskName, armcompute.Disk{ - Location: ptr.To(location), + Location: new(location), Properties: &armcompute.DiskProperties{ CreationData: &armcompute.CreationData{ - CreateOption: ptr.To(armcompute.DiskCreateOptionEmpty), + CreateOption: new(armcompute.DiskCreateOptionEmpty), }, - DiskSizeGB: ptr.To[int32](10), // 10 GB disk + DiskSizeGB: new(int32(10)), // 10 GB disk }, SKU: &armcompute.DiskSKU{ - Name: ptr.To(armcompute.DiskStorageAccountTypesStandardLRS), + Name: new(armcompute.DiskStorageAccountTypesStandardLRS), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-disk"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-disk"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/compute-image_test.go b/sources/azure/integration-tests/compute-image_test.go index 78ceef2c..7b311b35 100644 --- a/sources/azure/integration-tests/compute-image_test.go +++ b/sources/azure/integration-tests/compute-image_test.go @@ -13,7 +13,6 @@ import ( "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" @@ -336,22 +335,22 @@ func createImage(ctx context.Context, client *armcompute.ImagesClient, resourceG // Create an image from a managed disk poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, imageName, armcompute.Image{ - Location: ptr.To(location), + Location: new(location), Properties: &armcompute.ImageProperties{ - HyperVGeneration: ptr.To(armcompute.HyperVGenerationTypesV1), + HyperVGeneration: new(armcompute.HyperVGenerationTypesV1), StorageProfile: &armcompute.ImageStorageProfile{ OSDisk: &armcompute.ImageOSDisk{ ManagedDisk: &armcompute.SubResource{ - ID: ptr.To(sourceDiskID), + ID: new(sourceDiskID), }, - OSState: ptr.To(armcompute.OperatingSystemStateTypesGeneralized), - OSType: ptr.To(armcompute.OperatingSystemTypesLinux), + OSState: new(armcompute.OperatingSystemStateTypesGeneralized), + OSType: new(armcompute.OperatingSystemTypesLinux), }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-image"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-image"), }, }, nil) if err != nil { 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 7280cf75..7c4cfdb8 100644 --- a/sources/azure/integration-tests/compute-proximity-placement-group_test.go +++ b/sources/azure/integration-tests/compute-proximity-placement-group_test.go @@ -13,7 +13,6 @@ import ( "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" @@ -261,13 +260,13 @@ func createProximityPlacementGroup(ctx context.Context, client *armcompute.Proxi } resp, err := client.CreateOrUpdate(ctx, resourceGroupName, ppgName, armcompute.ProximityPlacementGroup{ - Location: ptr.To(location), + Location: new(location), Properties: &armcompute.ProximityPlacementGroupProperties{ - ProximityPlacementGroupType: ptr.To(armcompute.ProximityPlacementGroupTypeStandard), + ProximityPlacementGroupType: new(armcompute.ProximityPlacementGroupTypeStandard), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-proximity-placement-group"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-proximity-placement-group"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/compute-snapshot_test.go b/sources/azure/integration-tests/compute-snapshot_test.go index c6388e86..a3f24439 100644 --- a/sources/azure/integration-tests/compute-snapshot_test.go +++ b/sources/azure/integration-tests/compute-snapshot_test.go @@ -13,7 +13,6 @@ import ( "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" @@ -328,16 +327,16 @@ func createSnapshot(ctx context.Context, client *armcompute.SnapshotsClient, res } poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, snapshotName, armcompute.Snapshot{ - Location: ptr.To(location), + Location: new(location), Properties: &armcompute.SnapshotProperties{ CreationData: &armcompute.CreationData{ - CreateOption: ptr.To(armcompute.DiskCreateOptionCopy), - SourceResourceID: ptr.To(sourceDiskID), + CreateOption: new(armcompute.DiskCreateOptionCopy), + SourceResourceID: new(sourceDiskID), }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-snapshot"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-snapshot"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/compute-virtual-machine-extension_test.go b/sources/azure/integration-tests/compute-virtual-machine-extension_test.go index 89f45ec1..d2fdc25c 100644 --- a/sources/azure/integration-tests/compute-virtual-machine-extension_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine-extension_test.go @@ -14,7 +14,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "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" @@ -336,23 +335,23 @@ func createVirtualNetworkForExtension(ctx context.Context, client *armnetwork.Vi // Create the VNet poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vnetName, armnetwork.VirtualNetwork{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.VirtualNetworkPropertiesFormat{ AddressSpace: &armnetwork.AddressSpace{ - AddressPrefixes: []*string{ptr.To("10.2.0.0/16")}, + AddressPrefixes: []*string{new("10.2.0.0/16")}, }, Subnets: []*armnetwork.Subnet{ { - Name: ptr.To(integrationTestExtensionSubnetName), + Name: new(integrationTestExtensionSubnetName), Properties: &armnetwork.SubnetPropertiesFormat{ - AddressPrefix: ptr.To("10.2.0.0/24"), + AddressPrefix: new("10.2.0.0/24"), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-virtual-machine-extension"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-virtual-machine-extension"), }, }, nil) if err != nil { @@ -379,23 +378,23 @@ func createNetworkInterfaceForExtension(ctx context.Context, client *armnetwork. // Create the NIC poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, nicName, armnetwork.Interface{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.InterfacePropertiesFormat{ IPConfigurations: []*armnetwork.InterfaceIPConfiguration{ { - Name: ptr.To("ipconfig1"), + Name: new("ipconfig1"), Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{ Subnet: &armnetwork.Subnet{ - ID: ptr.To(subnetID), + ID: new(subnetID), }, - PrivateIPAllocationMethod: ptr.To(armnetwork.IPAllocationMethodDynamic), + PrivateIPAllocationMethod: new(armnetwork.IPAllocationMethodDynamic), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-virtual-machine-extension"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-virtual-machine-extension"), }, }, nil) if err != nil { @@ -432,51 +431,51 @@ func createVirtualMachineForExtension(ctx context.Context, client *armcompute.Vi // Create the VM poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vmName, armcompute.VirtualMachine{ - Location: ptr.To(location), + Location: new(location), Properties: &armcompute.VirtualMachineProperties{ HardwareProfile: &armcompute.HardwareProfile{ // Use Standard_D2ps_v5 - ARM-based VM with good availability in westus2 - VMSize: ptr.To(armcompute.VirtualMachineSizeTypes("Standard_D2ps_v5")), + VMSize: new(armcompute.VirtualMachineSizeTypes("Standard_D2ps_v5")), }, StorageProfile: &armcompute.StorageProfile{ ImageReference: &armcompute.ImageReference{ - Publisher: ptr.To("Canonical"), - Offer: ptr.To("0001-com-ubuntu-server-jammy"), - SKU: ptr.To("22_04-lts-arm64"), // ARM64 image for ARM-based VM - Version: ptr.To("latest"), + Publisher: new("Canonical"), + Offer: new("0001-com-ubuntu-server-jammy"), + SKU: new("22_04-lts-arm64"), // ARM64 image for ARM-based VM + Version: new("latest"), }, OSDisk: &armcompute.OSDisk{ - Name: ptr.To(fmt.Sprintf("%s-osdisk", vmName)), - CreateOption: ptr.To(armcompute.DiskCreateOptionTypesFromImage), + Name: new(fmt.Sprintf("%s-osdisk", vmName)), + CreateOption: new(armcompute.DiskCreateOptionTypesFromImage), ManagedDisk: &armcompute.ManagedDiskParameters{ - StorageAccountType: ptr.To(armcompute.StorageAccountTypesStandardLRS), + StorageAccountType: new(armcompute.StorageAccountTypesStandardLRS), }, - DeleteOption: ptr.To(armcompute.DiskDeleteOptionTypesDelete), + DeleteOption: new(armcompute.DiskDeleteOptionTypesDelete), }, }, OSProfile: &armcompute.OSProfile{ - ComputerName: ptr.To(vmName), - AdminUsername: ptr.To("azureuser"), + ComputerName: new(vmName), + AdminUsername: new("azureuser"), // Use password authentication for integration tests (simpler than SSH keys) - AdminPassword: ptr.To("OvmIntegTest2024!"), + AdminPassword: new("OvmIntegTest2024!"), LinuxConfiguration: &armcompute.LinuxConfiguration{ - DisablePasswordAuthentication: ptr.To(false), + DisablePasswordAuthentication: new(false), }, }, NetworkProfile: &armcompute.NetworkProfile{ NetworkInterfaces: []*armcompute.NetworkInterfaceReference{ { - ID: ptr.To(nicID), + ID: new(nicID), Properties: &armcompute.NetworkInterfaceReferenceProperties{ - Primary: ptr.To(true), + Primary: new(true), }, }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-virtual-machine-extension"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-virtual-machine-extension"), }, }, nil) if err != nil { @@ -563,18 +562,18 @@ func createVirtualMachineExtension(ctx context.Context, client *armcompute.Virtu // Create the extension with CustomScript extension // Reference: https://learn.microsoft.com/en-us/rest/api/compute/virtual-machine-extensions/create-or-update?view=rest-compute-2025-04-01&tabs=HTTP poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vmName, extensionName, armcompute.VirtualMachineExtension{ - Location: ptr.To(location), + Location: new(location), Properties: &armcompute.VirtualMachineExtensionProperties{ - Publisher: ptr.To("Microsoft.Azure.Extensions"), - Type: ptr.To("CustomScript"), - TypeHandlerVersion: ptr.To("2.1"), - Settings: map[string]interface{}{ + Publisher: new("Microsoft.Azure.Extensions"), + Type: new("CustomScript"), + TypeHandlerVersion: new("2.1"), + Settings: map[string]any{ "commandToExecute": "echo 'Hello from Overmind integration test'", }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-virtual-machine-extension"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-virtual-machine-extension"), }, }, nil) if err != nil { @@ -663,7 +662,7 @@ func deleteVirtualMachineExtension(ctx context.Context, client *armcompute.Virtu func deleteVirtualMachineForExtension(ctx context.Context, client *armcompute.VirtualMachinesClient, resourceGroupName, vmName string) error { // Use forceDeletion to speed up cleanup poller, err := client.BeginDelete(ctx, resourceGroupName, vmName, &armcompute.VirtualMachinesClientBeginDeleteOptions{ - ForceDeletion: ptr.To(true), + ForceDeletion: new(true), }) if err != nil { var respErr *azcore.ResponseError 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 9b18c877..4cb7cd8f 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 @@ -14,7 +14,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "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" @@ -336,23 +335,23 @@ func createVirtualNetworkForRunCommand(ctx context.Context, client *armnetwork.V // Create the VNet poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vnetName, armnetwork.VirtualNetwork{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.VirtualNetworkPropertiesFormat{ AddressSpace: &armnetwork.AddressSpace{ - AddressPrefixes: []*string{ptr.To("10.1.0.0/16")}, + AddressPrefixes: []*string{new("10.1.0.0/16")}, }, Subnets: []*armnetwork.Subnet{ { - Name: ptr.To(integrationTestRunCommandSubnetName), + Name: new(integrationTestRunCommandSubnetName), Properties: &armnetwork.SubnetPropertiesFormat{ - AddressPrefix: ptr.To("10.1.0.0/24"), + AddressPrefix: new("10.1.0.0/24"), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-virtual-machine-run-command"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-virtual-machine-run-command"), }, }, nil) if err != nil { @@ -379,23 +378,23 @@ func createNetworkInterfaceForRunCommand(ctx context.Context, client *armnetwork // Create the NIC poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, nicName, armnetwork.Interface{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.InterfacePropertiesFormat{ IPConfigurations: []*armnetwork.InterfaceIPConfiguration{ { - Name: ptr.To("ipconfig1"), + Name: new("ipconfig1"), Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{ Subnet: &armnetwork.Subnet{ - ID: ptr.To(subnetID), + ID: new(subnetID), }, - PrivateIPAllocationMethod: ptr.To(armnetwork.IPAllocationMethodDynamic), + PrivateIPAllocationMethod: new(armnetwork.IPAllocationMethodDynamic), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-virtual-machine-run-command"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-virtual-machine-run-command"), }, }, nil) if err != nil { @@ -432,51 +431,51 @@ func createVirtualMachineForRunCommand(ctx context.Context, client *armcompute.V // Create the VM poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vmName, armcompute.VirtualMachine{ - Location: ptr.To(location), + Location: new(location), Properties: &armcompute.VirtualMachineProperties{ HardwareProfile: &armcompute.HardwareProfile{ // Use Standard_D2ps_v5 - ARM-based VM with good availability in westus2 - VMSize: ptr.To(armcompute.VirtualMachineSizeTypes("Standard_D2ps_v5")), + VMSize: new(armcompute.VirtualMachineSizeTypes("Standard_D2ps_v5")), }, StorageProfile: &armcompute.StorageProfile{ ImageReference: &armcompute.ImageReference{ - Publisher: ptr.To("Canonical"), - Offer: ptr.To("0001-com-ubuntu-server-jammy"), - SKU: ptr.To("22_04-lts-arm64"), // ARM64 image for ARM-based VM - Version: ptr.To("latest"), + Publisher: new("Canonical"), + Offer: new("0001-com-ubuntu-server-jammy"), + SKU: new("22_04-lts-arm64"), // ARM64 image for ARM-based VM + Version: new("latest"), }, OSDisk: &armcompute.OSDisk{ - Name: ptr.To(fmt.Sprintf("%s-osdisk", vmName)), - CreateOption: ptr.To(armcompute.DiskCreateOptionTypesFromImage), + Name: new(fmt.Sprintf("%s-osdisk", vmName)), + CreateOption: new(armcompute.DiskCreateOptionTypesFromImage), ManagedDisk: &armcompute.ManagedDiskParameters{ - StorageAccountType: ptr.To(armcompute.StorageAccountTypesStandardLRS), + StorageAccountType: new(armcompute.StorageAccountTypesStandardLRS), }, - DeleteOption: ptr.To(armcompute.DiskDeleteOptionTypesDelete), + DeleteOption: new(armcompute.DiskDeleteOptionTypesDelete), }, }, OSProfile: &armcompute.OSProfile{ - ComputerName: ptr.To(vmName), - AdminUsername: ptr.To("azureuser"), + ComputerName: new(vmName), + AdminUsername: new("azureuser"), // Use password authentication for integration tests (simpler than SSH keys) - AdminPassword: ptr.To("OvmIntegTest2024!"), + AdminPassword: new("OvmIntegTest2024!"), LinuxConfiguration: &armcompute.LinuxConfiguration{ - DisablePasswordAuthentication: ptr.To(false), + DisablePasswordAuthentication: new(false), }, }, NetworkProfile: &armcompute.NetworkProfile{ NetworkInterfaces: []*armcompute.NetworkInterfaceReference{ { - ID: ptr.To(nicID), + ID: new(nicID), Properties: &armcompute.NetworkInterfaceReferenceProperties{ - Primary: ptr.To(true), + Primary: new(true), }, }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-virtual-machine-run-command"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-virtual-machine-run-command"), }, }, nil) if err != nil { @@ -563,18 +562,18 @@ func createVirtualMachineRunCommand(ctx context.Context, client *armcompute.Virt // Create the run command with a simple shell script // Reference: https://learn.microsoft.com/en-us/rest/api/compute/virtual-machine-run-commands/create-or-update?view=rest-compute-2025-04-01&tabs=HTTP poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vmName, runCommandName, armcompute.VirtualMachineRunCommand{ - Location: ptr.To(location), + Location: new(location), Properties: &armcompute.VirtualMachineRunCommandProperties{ Source: &armcompute.VirtualMachineRunCommandScriptSource{ - Script: ptr.To("#!/bin/bash\necho 'Hello from Overmind integration test'\n"), + Script: new("#!/bin/bash\necho 'Hello from Overmind integration test'\n"), }, - AsyncExecution: ptr.To(false), - RunAsUser: ptr.To("azureuser"), - TimeoutInSeconds: ptr.To[int32](3600), + AsyncExecution: new(false), + RunAsUser: new("azureuser"), + TimeoutInSeconds: new(int32(3600)), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-virtual-machine-run-command"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-virtual-machine-run-command"), }, }, nil) if err != nil { @@ -663,7 +662,7 @@ func deleteVirtualMachineRunCommand(ctx context.Context, client *armcompute.Virt func deleteVirtualMachineForRunCommand(ctx context.Context, client *armcompute.VirtualMachinesClient, resourceGroupName, vmName string) error { // Use forceDeletion to speed up cleanup poller, err := client.BeginDelete(ctx, resourceGroupName, vmName, &armcompute.VirtualMachinesClientBeginDeleteOptions{ - ForceDeletion: ptr.To(true), + ForceDeletion: new(true), }) if err != nil { var respErr *azcore.ResponseError 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 f7a723aa..3a79bec0 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 @@ -15,7 +15,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "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" @@ -342,22 +341,22 @@ func createVirtualNetworkForVMSS(ctx context.Context, client *armnetwork.Virtual // Create the VNet poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vnetName, armnetwork.VirtualNetwork{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.VirtualNetworkPropertiesFormat{ AddressSpace: &armnetwork.AddressSpace{ - AddressPrefixes: []*string{ptr.To("10.1.0.0/16")}, + AddressPrefixes: []*string{new("10.1.0.0/16")}, }, Subnets: []*armnetwork.Subnet{ { - Name: ptr.To(integrationTestVMSSSubnetName), + Name: new(integrationTestVMSSSubnetName), Properties: &armnetwork.SubnetPropertiesFormat{ - AddressPrefix: ptr.To("10.1.0.0/24"), + AddressPrefix: new("10.1.0.0/24"), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), + "purpose": new("overmind-integration-tests"), }, }, nil) if err != nil { @@ -408,53 +407,53 @@ func createVirtualMachineScaleSet(ctx context.Context, client *armcompute.Virtua // Create the VMSS poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vmssName, armcompute.VirtualMachineScaleSet{ - Location: ptr.To(location), + Location: new(location), SKU: &armcompute.SKU{ - Name: ptr.To("Standard_B1s"), // Burstable B-series VM - cheaper and more widely available - Tier: ptr.To("Standard"), - Capacity: ptr.To[int64](1), // Start with 1 instance for testing + Name: new("Standard_B1s"), // Burstable B-series VM - cheaper and more widely available + Tier: new("Standard"), + Capacity: new(int64(1)), // Start with 1 instance for testing }, Properties: &armcompute.VirtualMachineScaleSetProperties{ UpgradePolicy: &armcompute.UpgradePolicy{ - Mode: ptr.To(armcompute.UpgradeModeManual), + Mode: new(armcompute.UpgradeModeManual), }, VirtualMachineProfile: &armcompute.VirtualMachineScaleSetVMProfile{ OSProfile: &armcompute.VirtualMachineScaleSetOSProfile{ - ComputerNamePrefix: ptr.To(vmssName), - AdminUsername: ptr.To("azureuser"), - AdminPassword: ptr.To("OvmIntegTest2024!"), + ComputerNamePrefix: new(vmssName), + AdminUsername: new("azureuser"), + AdminPassword: new("OvmIntegTest2024!"), LinuxConfiguration: &armcompute.LinuxConfiguration{ - DisablePasswordAuthentication: ptr.To(false), + DisablePasswordAuthentication: new(false), }, }, StorageProfile: &armcompute.VirtualMachineScaleSetStorageProfile{ ImageReference: &armcompute.ImageReference{ - Publisher: ptr.To("Canonical"), - Offer: ptr.To("0001-com-ubuntu-server-jammy"), - SKU: ptr.To("22_04-lts"), // x64 image for B-series VM - Version: ptr.To("latest"), + Publisher: new("Canonical"), + Offer: new("0001-com-ubuntu-server-jammy"), + SKU: new("22_04-lts"), // x64 image for B-series VM + Version: new("latest"), }, OSDisk: &armcompute.VirtualMachineScaleSetOSDisk{ - CreateOption: ptr.To(armcompute.DiskCreateOptionTypesFromImage), + CreateOption: new(armcompute.DiskCreateOptionTypesFromImage), ManagedDisk: &armcompute.VirtualMachineScaleSetManagedDiskParameters{ - StorageAccountType: ptr.To(armcompute.StorageAccountTypesStandardLRS), + StorageAccountType: new(armcompute.StorageAccountTypesStandardLRS), }, }, }, NetworkProfile: &armcompute.VirtualMachineScaleSetNetworkProfile{ NetworkInterfaceConfigurations: []*armcompute.VirtualMachineScaleSetNetworkConfiguration{ { - Name: ptr.To("vmss-nic-config"), + Name: new("vmss-nic-config"), Properties: &armcompute.VirtualMachineScaleSetNetworkConfigurationProperties{ - Primary: ptr.To(true), + Primary: new(true), IPConfigurations: []*armcompute.VirtualMachineScaleSetIPConfiguration{ { - Name: ptr.To("ipconfig1"), + Name: new("ipconfig1"), Properties: &armcompute.VirtualMachineScaleSetIPConfigurationProperties{ Subnet: &armcompute.APIEntityReference{ - ID: ptr.To(subnetID), + ID: new(subnetID), }, - Primary: ptr.To(true), + Primary: new(true), }, }, }, @@ -465,8 +464,8 @@ func createVirtualMachineScaleSet(ctx context.Context, client *armcompute.Virtua }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-virtual-machine-scale-set"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-virtual-machine-scale-set"), }, }, nil) if err != nil { @@ -485,53 +484,53 @@ func createVirtualMachineScaleSet(ctx context.Context, client *armcompute.Virtua // Retry creation retryPoller, retryErr := client.BeginCreateOrUpdate(ctx, resourceGroupName, vmssName, armcompute.VirtualMachineScaleSet{ - Location: ptr.To(location), + Location: new(location), SKU: &armcompute.SKU{ - Name: ptr.To("Standard_B1s"), - Tier: ptr.To("Standard"), - Capacity: ptr.To[int64](1), + Name: new("Standard_B1s"), + Tier: new("Standard"), + Capacity: new(int64(1)), }, Properties: &armcompute.VirtualMachineScaleSetProperties{ UpgradePolicy: &armcompute.UpgradePolicy{ - Mode: ptr.To(armcompute.UpgradeModeManual), + Mode: new(armcompute.UpgradeModeManual), }, VirtualMachineProfile: &armcompute.VirtualMachineScaleSetVMProfile{ OSProfile: &armcompute.VirtualMachineScaleSetOSProfile{ - ComputerNamePrefix: ptr.To(vmssName), - AdminUsername: ptr.To("azureuser"), - AdminPassword: ptr.To("OvmIntegTest2024!"), + ComputerNamePrefix: new(vmssName), + AdminUsername: new("azureuser"), + AdminPassword: new("OvmIntegTest2024!"), LinuxConfiguration: &armcompute.LinuxConfiguration{ - DisablePasswordAuthentication: ptr.To(false), + DisablePasswordAuthentication: new(false), }, }, StorageProfile: &armcompute.VirtualMachineScaleSetStorageProfile{ ImageReference: &armcompute.ImageReference{ - Publisher: ptr.To("Canonical"), - Offer: ptr.To("0001-com-ubuntu-server-jammy"), - SKU: ptr.To("22_04-lts"), - Version: ptr.To("latest"), + Publisher: new("Canonical"), + Offer: new("0001-com-ubuntu-server-jammy"), + SKU: new("22_04-lts"), + Version: new("latest"), }, OSDisk: &armcompute.VirtualMachineScaleSetOSDisk{ - CreateOption: ptr.To(armcompute.DiskCreateOptionTypesFromImage), + CreateOption: new(armcompute.DiskCreateOptionTypesFromImage), ManagedDisk: &armcompute.VirtualMachineScaleSetManagedDiskParameters{ - StorageAccountType: ptr.To(armcompute.StorageAccountTypesStandardLRS), + StorageAccountType: new(armcompute.StorageAccountTypesStandardLRS), }, }, }, NetworkProfile: &armcompute.VirtualMachineScaleSetNetworkProfile{ NetworkInterfaceConfigurations: []*armcompute.VirtualMachineScaleSetNetworkConfiguration{ { - Name: ptr.To("vmss-nic-config"), + Name: new("vmss-nic-config"), Properties: &armcompute.VirtualMachineScaleSetNetworkConfigurationProperties{ - Primary: ptr.To(true), + Primary: new(true), IPConfigurations: []*armcompute.VirtualMachineScaleSetIPConfiguration{ { - Name: ptr.To("ipconfig1"), + Name: new("ipconfig1"), Properties: &armcompute.VirtualMachineScaleSetIPConfigurationProperties{ Subnet: &armcompute.APIEntityReference{ - ID: ptr.To(subnetID), + ID: new(subnetID), }, - Primary: ptr.To(true), + Primary: new(true), }, }, }, @@ -542,8 +541,8 @@ func createVirtualMachineScaleSet(ctx context.Context, client *armcompute.Virtua }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-virtual-machine-scale-set"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-virtual-machine-scale-set"), }, }, nil) if retryErr != nil { @@ -680,7 +679,7 @@ func waitForVMSSAvailable(ctx context.Context, client *armcompute.VirtualMachine func deleteVirtualMachineScaleSet(ctx context.Context, client *armcompute.VirtualMachineScaleSetsClient, resourceGroupName, vmssName string) error { // Use forceDeletion to speed up cleanup poller, err := client.BeginDelete(ctx, resourceGroupName, vmssName, &armcompute.VirtualMachineScaleSetsClientBeginDeleteOptions{ - ForceDeletion: ptr.To(true), + ForceDeletion: new(true), }) if err != nil { var respErr *azcore.ResponseError diff --git a/sources/azure/integration-tests/compute-virtual-machine_test.go b/sources/azure/integration-tests/compute-virtual-machine_test.go index 0b78f1bb..dfb76466 100644 --- a/sources/azure/integration-tests/compute-virtual-machine_test.go +++ b/sources/azure/integration-tests/compute-virtual-machine_test.go @@ -14,7 +14,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "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" @@ -307,22 +306,22 @@ func createVirtualNetwork(ctx context.Context, client *armnetwork.VirtualNetwork // Create the VNet poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vnetName, armnetwork.VirtualNetwork{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.VirtualNetworkPropertiesFormat{ AddressSpace: &armnetwork.AddressSpace{ - AddressPrefixes: []*string{ptr.To("10.0.0.0/16")}, + AddressPrefixes: []*string{new("10.0.0.0/16")}, }, Subnets: []*armnetwork.Subnet{ { - Name: ptr.To(integrationTestSubnetName), + Name: new(integrationTestSubnetName), Properties: &armnetwork.SubnetPropertiesFormat{ - AddressPrefix: ptr.To("10.0.0.0/24"), + AddressPrefix: new("10.0.0.0/24"), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), + "purpose": new("overmind-integration-tests"), }, }, nil) if err != nil { @@ -349,22 +348,22 @@ func createNetworkInterface(ctx context.Context, client *armnetwork.InterfacesCl // Create the NIC poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, nicName, armnetwork.Interface{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.InterfacePropertiesFormat{ IPConfigurations: []*armnetwork.InterfaceIPConfiguration{ { - Name: ptr.To("ipconfig1"), + Name: new("ipconfig1"), Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{ Subnet: &armnetwork.Subnet{ - ID: ptr.To(subnetID), + ID: new(subnetID), }, - PrivateIPAllocationMethod: ptr.To(armnetwork.IPAllocationMethodDynamic), + PrivateIPAllocationMethod: new(armnetwork.IPAllocationMethodDynamic), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), + "purpose": new("overmind-integration-tests"), }, }, nil) if err != nil { @@ -401,51 +400,51 @@ func createVirtualMachine(ctx context.Context, client *armcompute.VirtualMachine // Create the VM poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vmName, armcompute.VirtualMachine{ - Location: ptr.To(location), + Location: new(location), Properties: &armcompute.VirtualMachineProperties{ HardwareProfile: &armcompute.HardwareProfile{ // Use Standard_D2ps_v5 - ARM-based VM with good availability in westus2 - VMSize: ptr.To(armcompute.VirtualMachineSizeTypes("Standard_D2ps_v5")), + VMSize: new(armcompute.VirtualMachineSizeTypes("Standard_D2ps_v5")), }, StorageProfile: &armcompute.StorageProfile{ ImageReference: &armcompute.ImageReference{ - Publisher: ptr.To("Canonical"), - Offer: ptr.To("0001-com-ubuntu-server-jammy"), - SKU: ptr.To("22_04-lts-arm64"), // ARM64 image for ARM-based VM - Version: ptr.To("latest"), + Publisher: new("Canonical"), + Offer: new("0001-com-ubuntu-server-jammy"), + SKU: new("22_04-lts-arm64"), // ARM64 image for ARM-based VM + Version: new("latest"), }, OSDisk: &armcompute.OSDisk{ - Name: ptr.To(fmt.Sprintf("%s-osdisk", vmName)), - CreateOption: ptr.To(armcompute.DiskCreateOptionTypesFromImage), + Name: new(fmt.Sprintf("%s-osdisk", vmName)), + CreateOption: new(armcompute.DiskCreateOptionTypesFromImage), ManagedDisk: &armcompute.ManagedDiskParameters{ - StorageAccountType: ptr.To(armcompute.StorageAccountTypesStandardLRS), + StorageAccountType: new(armcompute.StorageAccountTypesStandardLRS), }, - DeleteOption: ptr.To(armcompute.DiskDeleteOptionTypesDelete), + DeleteOption: new(armcompute.DiskDeleteOptionTypesDelete), }, }, OSProfile: &armcompute.OSProfile{ - ComputerName: ptr.To(vmName), - AdminUsername: ptr.To("azureuser"), + ComputerName: new(vmName), + AdminUsername: new("azureuser"), // Use password authentication for integration tests (simpler than SSH keys) - AdminPassword: ptr.To("OvmIntegTest2024!"), + AdminPassword: new("OvmIntegTest2024!"), LinuxConfiguration: &armcompute.LinuxConfiguration{ - DisablePasswordAuthentication: ptr.To(false), + DisablePasswordAuthentication: new(false), }, }, NetworkProfile: &armcompute.NetworkProfile{ NetworkInterfaces: []*armcompute.NetworkInterfaceReference{ { - ID: ptr.To(nicID), + ID: new(nicID), Properties: &armcompute.NetworkInterfaceReferenceProperties{ - Primary: ptr.To(true), + Primary: new(true), }, }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("compute-virtual-machine"), + "purpose": new("overmind-integration-tests"), + "test": new("compute-virtual-machine"), }, }, nil) if err != nil { @@ -525,7 +524,7 @@ func waitForVMAvailable(ctx context.Context, client *armcompute.VirtualMachinesC func deleteVirtualMachine(ctx context.Context, client *armcompute.VirtualMachinesClient, resourceGroupName, vmName string) error { // Use forceDeletion to speed up cleanup poller, err := client.BeginDelete(ctx, resourceGroupName, vmName, &armcompute.VirtualMachinesClientBeginDeleteOptions{ - ForceDeletion: ptr.To(true), + ForceDeletion: new(true), }) if err != nil { var respErr *azcore.ResponseError diff --git a/sources/azure/integration-tests/dbforpostgresql-database_test.go b/sources/azure/integration-tests/dbforpostgresql-database_test.go index e2efc7ed..2b685d94 100644 --- a/sources/azure/integration-tests/dbforpostgresql-database_test.go +++ b/sources/azure/integration-tests/dbforpostgresql-database_test.go @@ -15,7 +15,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" "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" @@ -309,23 +308,23 @@ func createPostgreSQLFlexibleServer(ctx context.Context, client *armpostgresqlfl // Create the PostgreSQL Flexible Server // Using Burstable tier for cost-effective testing poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, serverName, armpostgresqlflexibleservers.Server{ - Location: ptr.To(location), + Location: new(location), Properties: &armpostgresqlflexibleservers.ServerProperties{ - AdministratorLogin: ptr.To(adminLogin), - AdministratorLoginPassword: ptr.To(adminPassword), - Version: ptr.To(armpostgresqlflexibleservers.PostgresMajorVersion("14")), - Storage: &armpostgresqlflexibleservers.Storage{StorageSizeGB: ptr.To[int32](32)}, - Backup: &armpostgresqlflexibleservers.Backup{BackupRetentionDays: ptr.To[int32](7), GeoRedundantBackup: ptr.To(armpostgresqlflexibleservers.GeographicallyRedundantBackupDisabled)}, - Network: &armpostgresqlflexibleservers.Network{PublicNetworkAccess: ptr.To(armpostgresqlflexibleservers.ServerPublicNetworkAccessStateEnabled)}, + AdministratorLogin: new(adminLogin), + AdministratorLoginPassword: new(adminPassword), + Version: new(armpostgresqlflexibleservers.PostgresMajorVersion("14")), + Storage: &armpostgresqlflexibleservers.Storage{StorageSizeGB: new(int32(32))}, + Backup: &armpostgresqlflexibleservers.Backup{BackupRetentionDays: new(int32(7)), GeoRedundantBackup: new(armpostgresqlflexibleservers.GeographicallyRedundantBackupDisabled)}, + Network: &armpostgresqlflexibleservers.Network{PublicNetworkAccess: new(armpostgresqlflexibleservers.ServerPublicNetworkAccessStateEnabled)}, HighAvailability: nil, // High availability disabled by not setting it }, SKU: &armpostgresqlflexibleservers.SKU{ - Name: ptr.To("Standard_B1ms"), // Burstable tier, 1 vCore, 2GB RAM - Tier: ptr.To(armpostgresqlflexibleservers.SKUTierBurstable), + Name: new("Standard_B1ms"), // Burstable tier, 1 vCore, 2GB RAM + Tier: new(armpostgresqlflexibleservers.SKUTierBurstable), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("dbforpostgresql-database"), + "purpose": new("overmind-integration-tests"), + "test": new("dbforpostgresql-database"), }, }, nil) if err != nil { @@ -407,8 +406,8 @@ func createPostgreSQLDatabase(ctx context.Context, client *armpostgresqlflexible // Create the PostgreSQL database poller, err := client.BeginCreate(ctx, resourceGroupName, serverName, databaseName, armpostgresqlflexibleservers.Database{ Properties: &armpostgresqlflexibleservers.DatabaseProperties{ - Charset: ptr.To("UTF8"), - Collation: ptr.To("en_US.utf8"), + Charset: new("UTF8"), + Collation: new("en_US.utf8"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/documentdb-database-accounts_test.go b/sources/azure/integration-tests/documentdb-database-accounts_test.go index 6d59dbb4..69ea4ce8 100644 --- a/sources/azure/integration-tests/documentdb-database-accounts_test.go +++ b/sources/azure/integration-tests/documentdb-database-accounts_test.go @@ -13,7 +13,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" "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/sdpcache" @@ -189,24 +188,24 @@ func createCosmosDBAccount(ctx context.Context, client *armcosmos.DatabaseAccoun // Create the Cosmos DB account // Using SQL API as the default, which is the most common poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, accountName, armcosmos.DatabaseAccountCreateUpdateParameters{ - Location: ptr.To(location), - Kind: ptr.To(armcosmos.DatabaseAccountKindGlobalDocumentDB), + Location: new(location), + Kind: new(armcosmos.DatabaseAccountKindGlobalDocumentDB), Properties: &armcosmos.DatabaseAccountCreateUpdateProperties{ - DatabaseAccountOfferType: ptr.To("Standard"), + DatabaseAccountOfferType: new("Standard"), Locations: []*armcosmos.Location{ { - LocationName: ptr.To(location), - FailoverPriority: ptr.To[int32](0), - IsZoneRedundant: ptr.To(false), + LocationName: new(location), + FailoverPriority: new(int32(0)), + IsZoneRedundant: new(false), }, }, ConsistencyPolicy: &armcosmos.ConsistencyPolicy{ - DefaultConsistencyLevel: ptr.To(armcosmos.DefaultConsistencyLevelSession), + DefaultConsistencyLevel: new(armcosmos.DefaultConsistencyLevelSession), }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("documentdb-database-accounts"), + "purpose": new("overmind-integration-tests"), + "test": new("documentdb-database-accounts"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/helpers_test.go b/sources/azure/integration-tests/helpers_test.go index 8c27e165..7690f315 100644 --- a/sources/azure/integration-tests/helpers_test.go +++ b/sources/azure/integration-tests/helpers_test.go @@ -6,7 +6,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" ) // Shared constants for integration tests @@ -26,10 +25,10 @@ func createResourceGroup(ctx context.Context, client *armresources.ResourceGroup // Create the resource group _, err = client.CreateOrUpdate(ctx, resourceGroupName, armresources.ResourceGroup{ - Location: ptr.To(location), + Location: new(location), Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "managed": ptr.To("true"), + "purpose": new("overmind-integration-tests"), + "managed": new("true"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/keyvault-vault_test.go b/sources/azure/integration-tests/keyvault-vault_test.go index 187756c2..614e9dfb 100644 --- a/sources/azure/integration-tests/keyvault-vault_test.go +++ b/sources/azure/integration-tests/keyvault-vault_test.go @@ -13,7 +13,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -212,20 +211,20 @@ func createKeyVault(ctx context.Context, client *armkeyvault.VaultsClient, resou // Key Vault names must be globally unique and 3-24 characters // They can only contain alphanumeric characters and hyphens params := armkeyvault.VaultCreateOrUpdateParameters{ - Location: ptr.To(location), + Location: new(location), Properties: &armkeyvault.VaultProperties{ - TenantID: ptr.To(tenantID), + TenantID: new(tenantID), SKU: &armkeyvault.SKU{ - Family: ptr.To(armkeyvault.SKUFamilyA), - Name: ptr.To(armkeyvault.SKUNameStandard), + Family: new(armkeyvault.SKUFamilyA), + Name: new(armkeyvault.SKUNameStandard), }, AccessPolicies: []*armkeyvault.AccessPolicyEntry{ // For integration tests, we create with minimal configuration. }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("keyvault-vault"), + "purpose": new("overmind-integration-tests"), + "test": new("keyvault-vault"), }, } 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 a622263a..6330e078 100644 --- a/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go +++ b/sources/azure/integration-tests/managedidentity-user-assigned-identity_test.go @@ -13,7 +13,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -255,10 +254,10 @@ func createUserAssignedIdentity(ctx context.Context, client *armmsi.UserAssigned // Create the User Assigned Identity resp, err := client.CreateOrUpdate(ctx, resourceGroupName, identityName, armmsi.Identity{ - Location: ptr.To(location), + Location: new(location), Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("managedidentity-user-assigned-identity"), + "purpose": new("overmind-integration-tests"), + "test": new("managedidentity-user-assigned-identity"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/network-application-gateway_test.go b/sources/azure/integration-tests/network-application-gateway_test.go index a6e6e899..bbcbbefa 100644 --- a/sources/azure/integration-tests/network-application-gateway_test.go +++ b/sources/azure/integration-tests/network-application-gateway_test.go @@ -13,7 +13,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "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" @@ -374,14 +373,14 @@ func createVirtualNetworkForAG(ctx context.Context, client *armnetwork.VirtualNe // Create the VNet poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vnetName, armnetwork.VirtualNetwork{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.VirtualNetworkPropertiesFormat{ AddressSpace: &armnetwork.AddressSpace{ - AddressPrefixes: []*string{ptr.To("10.3.0.0/16")}, + AddressPrefixes: []*string{new("10.3.0.0/16")}, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), + "purpose": new("overmind-integration-tests"), }, }, nil) if err != nil { @@ -410,7 +409,7 @@ func createAGSubnet(ctx context.Context, client *armnetwork.SubnetsClient, resou // Create the subnet with /24 address space for Application Gateway poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vnetName, subnetName, armnetwork.Subnet{ Properties: &armnetwork.SubnetPropertiesFormat{ - AddressPrefix: ptr.To("10.3.0.0/24"), + AddressPrefix: new("10.3.0.0/24"), }, }, nil) if err != nil { @@ -463,16 +462,16 @@ func createPublicIPForAG(ctx context.Context, client *armnetwork.PublicIPAddress // Create the public IP address with Standard SKU (required for Application Gateway v2) poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, publicIPName, armnetwork.PublicIPAddress{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.PublicIPAddressPropertiesFormat{ - PublicIPAllocationMethod: ptr.To(armnetwork.IPAllocationMethodStatic), - PublicIPAddressVersion: ptr.To(armnetwork.IPVersionIPv4), + PublicIPAllocationMethod: new(armnetwork.IPAllocationMethodStatic), + PublicIPAddressVersion: new(armnetwork.IPVersionIPv4), }, SKU: &armnetwork.PublicIPAddressSKU{ - Name: ptr.To(armnetwork.PublicIPAddressSKUNameStandard), + Name: new(armnetwork.PublicIPAddressSKUNameStandard), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), + "purpose": new("overmind-integration-tests"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/network-load-balancer_test.go b/sources/azure/integration-tests/network-load-balancer_test.go index cd23793c..3746285b 100644 --- a/sources/azure/integration-tests/network-load-balancer_test.go +++ b/sources/azure/integration-tests/network-load-balancer_test.go @@ -12,7 +12,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "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/sdpcache" @@ -369,22 +368,22 @@ func createVirtualNetworkForLB(ctx context.Context, client *armnetwork.VirtualNe // Create the VNet poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vnetName, armnetwork.VirtualNetwork{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.VirtualNetworkPropertiesFormat{ AddressSpace: &armnetwork.AddressSpace{ - AddressPrefixes: []*string{ptr.To("10.2.0.0/16")}, + AddressPrefixes: []*string{new("10.2.0.0/16")}, }, Subnets: []*armnetwork.Subnet{ { - Name: ptr.To(integrationTestSubnetNameForLB), + Name: new(integrationTestSubnetNameForLB), Properties: &armnetwork.SubnetPropertiesFormat{ - AddressPrefix: ptr.To("10.2.0.0/24"), + AddressPrefix: new("10.2.0.0/24"), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), + "purpose": new("overmind-integration-tests"), }, }, nil) if err != nil { @@ -432,16 +431,16 @@ func createPublicIPForLB(ctx context.Context, client *armnetwork.PublicIPAddress // Create the public IP address poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, publicIPName, armnetwork.PublicIPAddress{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.PublicIPAddressPropertiesFormat{ - PublicIPAllocationMethod: ptr.To(armnetwork.IPAllocationMethodStatic), - PublicIPAddressVersion: ptr.To(armnetwork.IPVersionIPv4), + PublicIPAllocationMethod: new(armnetwork.IPAllocationMethodStatic), + PublicIPAddressVersion: new(armnetwork.IPVersionIPv4), }, SKU: &armnetwork.PublicIPAddressSKU{ - Name: ptr.To(armnetwork.PublicIPAddressSKUNameStandard), + Name: new(armnetwork.PublicIPAddressSKUNameStandard), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), + "purpose": new("overmind-integration-tests"), }, }, nil) if err != nil { @@ -489,47 +488,47 @@ func createPublicLoadBalancer(ctx context.Context, client *armnetwork.LoadBalanc // Create the public load balancer poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, lbName, armnetwork.LoadBalancer{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.LoadBalancerPropertiesFormat{ FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{ { - Name: ptr.To("frontend-ip-config-public"), + Name: new("frontend-ip-config-public"), Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{ PublicIPAddress: &armnetwork.PublicIPAddress{ - ID: ptr.To(publicIPID), + ID: new(publicIPID), }, }, }, }, BackendAddressPools: []*armnetwork.BackendAddressPool{ { - Name: ptr.To("backend-pool"), + Name: new("backend-pool"), }, }, LoadBalancingRules: []*armnetwork.LoadBalancingRule{ { - Name: ptr.To("lb-rule"), + Name: new("lb-rule"), Properties: &armnetwork.LoadBalancingRulePropertiesFormat{ FrontendIPConfiguration: &armnetwork.SubResource{ - ID: ptr.To(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/frontendIPConfigurations/frontend-ip-config-public", os.Getenv("AZURE_SUBSCRIPTION_ID"), resourceGroupName, lbName)), + ID: new(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/frontendIPConfigurations/frontend-ip-config-public", os.Getenv("AZURE_SUBSCRIPTION_ID"), resourceGroupName, lbName)), }, BackendAddressPool: &armnetwork.SubResource{ - ID: ptr.To(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/backendAddressPools/backend-pool", os.Getenv("AZURE_SUBSCRIPTION_ID"), resourceGroupName, lbName)), + ID: new(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/backendAddressPools/backend-pool", os.Getenv("AZURE_SUBSCRIPTION_ID"), resourceGroupName, lbName)), }, - Protocol: ptr.To(armnetwork.TransportProtocolTCP), - FrontendPort: ptr.To[int32](80), - BackendPort: ptr.To[int32](80), - EnableFloatingIP: ptr.To(false), - IdleTimeoutInMinutes: ptr.To[int32](4), + Protocol: new(armnetwork.TransportProtocolTCP), + FrontendPort: new(int32(80)), + BackendPort: new(int32(80)), + EnableFloatingIP: new(false), + IdleTimeoutInMinutes: new(int32(4)), }, }, }, }, SKU: &armnetwork.LoadBalancerSKU{ - Name: ptr.To(armnetwork.LoadBalancerSKUNameStandard), + Name: new(armnetwork.LoadBalancerSKUNameStandard), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), + "purpose": new("overmind-integration-tests"), }, }, nil) if err != nil { @@ -556,49 +555,49 @@ func createInternalLoadBalancer(ctx context.Context, client *armnetwork.LoadBala // Create the internal load balancer poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, lbName, armnetwork.LoadBalancer{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.LoadBalancerPropertiesFormat{ FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{ { - Name: ptr.To("frontend-ip-config-internal"), + Name: new("frontend-ip-config-internal"), Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{ Subnet: &armnetwork.Subnet{ - ID: ptr.To(subnetID), + ID: new(subnetID), }, - PrivateIPAddress: ptr.To("10.2.0.5"), - PrivateIPAllocationMethod: ptr.To(armnetwork.IPAllocationMethodStatic), + PrivateIPAddress: new("10.2.0.5"), + PrivateIPAllocationMethod: new(armnetwork.IPAllocationMethodStatic), }, }, }, BackendAddressPools: []*armnetwork.BackendAddressPool{ { - Name: ptr.To("backend-pool"), + Name: new("backend-pool"), }, }, LoadBalancingRules: []*armnetwork.LoadBalancingRule{ { - Name: ptr.To("lb-rule"), + Name: new("lb-rule"), Properties: &armnetwork.LoadBalancingRulePropertiesFormat{ FrontendIPConfiguration: &armnetwork.SubResource{ - ID: ptr.To(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/frontendIPConfigurations/frontend-ip-config-internal", os.Getenv("AZURE_SUBSCRIPTION_ID"), resourceGroupName, lbName)), + ID: new(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/frontendIPConfigurations/frontend-ip-config-internal", os.Getenv("AZURE_SUBSCRIPTION_ID"), resourceGroupName, lbName)), }, BackendAddressPool: &armnetwork.SubResource{ - ID: ptr.To(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/backendAddressPools/backend-pool", os.Getenv("AZURE_SUBSCRIPTION_ID"), resourceGroupName, lbName)), + ID: new(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/loadBalancers/%s/backendAddressPools/backend-pool", os.Getenv("AZURE_SUBSCRIPTION_ID"), resourceGroupName, lbName)), }, - Protocol: ptr.To(armnetwork.TransportProtocolTCP), - FrontendPort: ptr.To[int32](80), - BackendPort: ptr.To[int32](80), - EnableFloatingIP: ptr.To(false), - IdleTimeoutInMinutes: ptr.To[int32](4), + Protocol: new(armnetwork.TransportProtocolTCP), + FrontendPort: new(int32(80)), + BackendPort: new(int32(80)), + EnableFloatingIP: new(false), + IdleTimeoutInMinutes: new(int32(4)), }, }, }, }, SKU: &armnetwork.LoadBalancerSKU{ - Name: ptr.To(armnetwork.LoadBalancerSKUNameStandard), + Name: new(armnetwork.LoadBalancerSKUNameStandard), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), + "purpose": new("overmind-integration-tests"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/network-network-interface_test.go b/sources/azure/integration-tests/network-network-interface_test.go index 8e7c483d..c0aaf9e0 100644 --- a/sources/azure/integration-tests/network-network-interface_test.go +++ b/sources/azure/integration-tests/network-network-interface_test.go @@ -12,7 +12,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "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/sdpcache" @@ -253,22 +252,22 @@ func createVirtualNetworkForNIC(ctx context.Context, client *armnetwork.VirtualN // Create the VNet poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vnetName, armnetwork.VirtualNetwork{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.VirtualNetworkPropertiesFormat{ AddressSpace: &armnetwork.AddressSpace{ - AddressPrefixes: []*string{ptr.To("10.1.0.0/16")}, + AddressPrefixes: []*string{new("10.1.0.0/16")}, }, Subnets: []*armnetwork.Subnet{ { - Name: ptr.To(integrationTestSubnetNameForNIC), + Name: new(integrationTestSubnetNameForNIC), Properties: &armnetwork.SubnetPropertiesFormat{ - AddressPrefix: ptr.To("10.1.0.0/24"), + AddressPrefix: new("10.1.0.0/24"), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), + "purpose": new("overmind-integration-tests"), }, }, nil) if err != nil { 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 08de4270..75a1fcce 100644 --- a/sources/azure/integration-tests/network-network-security-group_test.go +++ b/sources/azure/integration-tests/network-network-security-group_test.go @@ -13,7 +13,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "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" @@ -318,27 +317,27 @@ func createNetworkSecurityGroup(ctx context.Context, client *armnetwork.Security // Create a basic network security group with a sample security rule // This creates an NSG with a default allow rule for testing poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, nsgName, armnetwork.SecurityGroup{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.SecurityGroupPropertiesFormat{ SecurityRules: []*armnetwork.SecurityRule{ { - Name: ptr.To("AllowSSH"), + Name: new("AllowSSH"), Properties: &armnetwork.SecurityRulePropertiesFormat{ - Protocol: ptr.To(armnetwork.SecurityRuleProtocolTCP), - SourcePortRange: ptr.To("*"), - DestinationPortRange: ptr.To("22"), - SourceAddressPrefix: ptr.To("*"), - DestinationAddressPrefix: ptr.To("*"), - Access: ptr.To(armnetwork.SecurityRuleAccessAllow), - Priority: ptr.To[int32](1000), - Direction: ptr.To(armnetwork.SecurityRuleDirectionInbound), + Protocol: new(armnetwork.SecurityRuleProtocolTCP), + SourcePortRange: new("*"), + DestinationPortRange: new("22"), + SourceAddressPrefix: new("*"), + DestinationAddressPrefix: new("*"), + Access: new(armnetwork.SecurityRuleAccessAllow), + Priority: new(int32(1000)), + Direction: new(armnetwork.SecurityRuleDirectionInbound), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("network-network-security-group"), + "purpose": new("overmind-integration-tests"), + "test": new("network-network-security-group"), }, }, nil) if err != nil { 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 f2eeab78..9a5f29c4 100644 --- a/sources/azure/integration-tests/network-public-ip-address_test.go +++ b/sources/azure/integration-tests/network-public-ip-address_test.go @@ -13,7 +13,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "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" @@ -296,22 +295,22 @@ func createVirtualNetworkForPIP(ctx context.Context, client *armnetwork.VirtualN // Create the VNet poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, vnetName, armnetwork.VirtualNetwork{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.VirtualNetworkPropertiesFormat{ AddressSpace: &armnetwork.AddressSpace{ - AddressPrefixes: []*string{ptr.To("10.2.0.0/16")}, + AddressPrefixes: []*string{new("10.2.0.0/16")}, }, Subnets: []*armnetwork.Subnet{ { - Name: ptr.To(integrationTestSubnetNameForPIP), + Name: new(integrationTestSubnetNameForPIP), Properties: &armnetwork.SubnetPropertiesFormat{ - AddressPrefix: ptr.To("10.2.0.0/24"), + AddressPrefix: new("10.2.0.0/24"), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), + "purpose": new("overmind-integration-tests"), }, }, nil) if err != nil { @@ -359,17 +358,17 @@ func createPublicIPAddress(ctx context.Context, client *armnetwork.PublicIPAddre // Create the public IP address poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, publicIPName, armnetwork.PublicIPAddress{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.PublicIPAddressPropertiesFormat{ - PublicIPAddressVersion: ptr.To(armnetwork.IPVersionIPv4), - PublicIPAllocationMethod: ptr.To(armnetwork.IPAllocationMethodStatic), + PublicIPAddressVersion: new(armnetwork.IPVersionIPv4), + PublicIPAllocationMethod: new(armnetwork.IPAllocationMethodStatic), }, SKU: &armnetwork.PublicIPAddressSKU{ - Name: ptr.To(armnetwork.PublicIPAddressSKUNameStandard), + Name: new(armnetwork.PublicIPAddressSKUNameStandard), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("network-public-ip-address"), + "purpose": new("overmind-integration-tests"), + "test": new("network-public-ip-address"), }, }, nil) if err != nil { @@ -463,26 +462,26 @@ func createNetworkInterfaceWithPublicIP(ctx context.Context, client *armnetwork. // Create the NIC poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, nicName, armnetwork.Interface{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.InterfacePropertiesFormat{ IPConfigurations: []*armnetwork.InterfaceIPConfiguration{ { - Name: ptr.To("ipconfig1"), + Name: new("ipconfig1"), Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{ Subnet: &armnetwork.Subnet{ - ID: ptr.To(subnetID), + ID: new(subnetID), }, PublicIPAddress: &armnetwork.PublicIPAddress{ - ID: ptr.To(publicIPID), + ID: new(publicIPID), }, - PrivateIPAllocationMethod: ptr.To(armnetwork.IPAllocationMethodDynamic), + PrivateIPAllocationMethod: new(armnetwork.IPAllocationMethodDynamic), }, }, }, }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("network-public-ip-address"), + "purpose": new("overmind-integration-tests"), + "test": new("network-public-ip-address"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/network-route-table_test.go b/sources/azure/integration-tests/network-route-table_test.go index f9342ef4..e788388b 100644 --- a/sources/azure/integration-tests/network-route-table_test.go +++ b/sources/azure/integration-tests/network-route-table_test.go @@ -13,7 +13,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "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" @@ -328,13 +327,13 @@ func createRouteTable(ctx context.Context, client *armnetwork.RouteTablesClient, // Create a basic route table poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, routeTableName, armnetwork.RouteTable{ - Location: ptr.To(location), + Location: new(location), Properties: &armnetwork.RouteTablePropertiesFormat{ // Routes will be added separately as child resources }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("network-route-table"), + "purpose": new("overmind-integration-tests"), + "test": new("network-route-table"), }, }, nil) if err != nil { @@ -433,9 +432,9 @@ func createRoute(ctx context.Context, client *armnetwork.RoutesClient, resourceG // This creates a route that will link to a NetworkIP poller, err := client.BeginCreateOrUpdate(ctx, resourceGroupName, routeTableName, routeName, armnetwork.Route{ Properties: &armnetwork.RoutePropertiesFormat{ - AddressPrefix: ptr.To("10.0.0.0/8"), - NextHopType: ptr.To(armnetwork.RouteNextHopTypeVirtualAppliance), - NextHopIPAddress: ptr.To("10.0.0.1"), // This will create a link to stdlib.NetworkIP + AddressPrefix: new("10.0.0.0/8"), + NextHopType: new(armnetwork.RouteNextHopTypeVirtualAppliance), + NextHopIPAddress: new("10.0.0.1"), // This will create a link to stdlib.NetworkIP }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/network-zone_test.go b/sources/azure/integration-tests/network-zone_test.go index cd2e363d..4a69d93a 100644 --- a/sources/azure/integration-tests/network-zone_test.go +++ b/sources/azure/integration-tests/network-zone_test.go @@ -13,7 +13,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" "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" @@ -344,13 +343,13 @@ func createDNSZone(ctx context.Context, client *armdns.ZonesClient, resourceGrou // Create the DNS zone resp, err := client.CreateOrUpdate(ctx, resourceGroupName, zoneName, armdns.Zone{ - Location: ptr.To(location), + Location: new(location), Properties: &armdns.ZoneProperties{ - ZoneType: ptr.To(armdns.ZoneTypePublic), + ZoneType: new(armdns.ZoneTypePublic), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "managed": ptr.To("true"), + "purpose": new("overmind-integration-tests"), + "managed": new("true"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/sql-database_test.go b/sources/azure/integration-tests/sql-database_test.go index be46d5a0..3b7b72ee 100644 --- a/sources/azure/integration-tests/sql-database_test.go +++ b/sources/azure/integration-tests/sql-database_test.go @@ -15,7 +15,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -299,7 +298,7 @@ func createSQLServer(ctx context.Context, client *armsql.ServersClient, resource } var respErr *azcore.ResponseError - if err != nil && !errors.As(err, &respErr) { + if !errors.As(err, &respErr) { // Some other error occurred return fmt.Errorf("failed to check if SQL server exists: %w", err) } @@ -321,15 +320,15 @@ func createSQLServer(ctx context.Context, client *armsql.ServersClient, resource } poller, err := client.BeginCreateOrUpdate(ctx, resourceGroup, serverName, armsql.Server{ - Location: ptr.To(location), + Location: new(location), Properties: &armsql.ServerProperties{ - AdministratorLogin: ptr.To(adminLogin), - AdministratorLoginPassword: ptr.To(adminPassword), - Version: ptr.To("12.0"), + AdministratorLogin: new(adminLogin), + AdministratorLoginPassword: new(adminPassword), + Version: new("12.0"), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "managed": ptr.To("true"), + "purpose": new("overmind-integration-tests"), + "managed": new("true"), }, }, nil) if err != nil { @@ -372,7 +371,7 @@ func createSQLDatabase(ctx context.Context, client *armsql.DatabasesClient, reso } var respErr *azcore.ResponseError - if err != nil && !errors.As(err, &respErr) { + if !errors.As(err, &respErr) { // Some other error occurred return fmt.Errorf("failed to check if SQL database exists: %w", err) } @@ -386,13 +385,13 @@ func createSQLDatabase(ctx context.Context, client *armsql.DatabasesClient, reso // Create the SQL database // Using Basic tier for integration tests (cheaper) poller, err := client.BeginCreateOrUpdate(ctx, resourceGroup, serverName, databaseName, armsql.Database{ - Location: ptr.To(location), + Location: new(location), Properties: &armsql.DatabaseProperties{ - RequestedServiceObjectiveName: ptr.To("Basic"), + RequestedServiceObjectiveName: new("Basic"), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "managed": ptr.To("true"), + "purpose": new("overmind-integration-tests"), + "managed": new("true"), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/storage-blob-container_test.go b/sources/azure/integration-tests/storage-blob-container_test.go index 0ff9b17d..ffda244e 100644 --- a/sources/azure/integration-tests/storage-blob-container_test.go +++ b/sources/azure/integration-tests/storage-blob-container_test.go @@ -15,7 +15,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -275,17 +274,17 @@ func createStorageAccount(ctx context.Context, client *armstorage.AccountsClient // Create the storage account poller, err := client.BeginCreate(ctx, resourceGroupName, accountName, armstorage.AccountCreateParameters{ - Location: ptr.To(location), - Kind: ptr.To(armstorage.KindStorageV2), + Location: new(location), + Kind: new(armstorage.KindStorageV2), SKU: &armstorage.SKU{ - Name: ptr.To(armstorage.SKUNameStandardLRS), + Name: new(armstorage.SKUNameStandardLRS), }, Properties: &armstorage.AccountPropertiesCreateParameters{ - AccessTier: ptr.To(armstorage.AccessTierHot), + AccessTier: new(armstorage.AccessTierHot), }, Tags: map[string]*string{ - "purpose": ptr.To("overmind-integration-tests"), - "test": ptr.To("storage-blob-container"), + "purpose": new("overmind-integration-tests"), + "test": new("storage-blob-container"), }, }, nil) if err != nil { @@ -372,7 +371,7 @@ func createBlobContainer(ctx context.Context, client *armstorage.BlobContainersC // Create the blob container resp, err := client.Create(ctx, resourceGroupName, accountName, containerName, armstorage.BlobContainer{ ContainerProperties: &armstorage.ContainerProperties{ - PublicAccess: ptr.To(armstorage.PublicAccessNone), + PublicAccess: new(armstorage.PublicAccessNone), }, }, nil) if err != nil { diff --git a/sources/azure/integration-tests/storage-fileshare_test.go b/sources/azure/integration-tests/storage-fileshare_test.go index d9a5a992..092e41d3 100644 --- a/sources/azure/integration-tests/storage-fileshare_test.go +++ b/sources/azure/integration-tests/storage-fileshare_test.go @@ -12,7 +12,6 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -249,7 +248,7 @@ func createFileShare(ctx context.Context, client *armstorage.FileSharesClient, r // File shares require a quota (size in GB) resp, err := client.Create(ctx, resourceGroupName, accountName, shareName, armstorage.FileShare{ FileShareProperties: &armstorage.FileShareProperties{ - ShareQuota: ptr.To(int32(1)), // 1GB minimum quota + ShareQuota: new(int32(1)), // 1GB minimum quota }, }, nil) if err != nil { diff --git a/sources/azure/manual/authorization-role-assignment_test.go b/sources/azure/manual/authorization-role-assignment_test.go index ae5c5f92..7a7fec01 100644 --- a/sources/azure/manual/authorization-role-assignment_test.go +++ b/sources/azure/manual/authorization-role-assignment_test.go @@ -6,7 +6,6 @@ import ( "reflect" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" "go.uber.org/mock/gomock" @@ -142,7 +141,7 @@ func TestAuthorizationRoleAssignment(t *testing.T) { roleAssignment := &armauthorization.RoleAssignment{ Name: nil, // Role assignment with nil name should cause error Properties: &armauthorization.RoleAssignmentProperties{ - Scope: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg"), + Scope: new("/subscriptions/test-subscription/resourceGroups/test-rg"), }, } @@ -271,7 +270,7 @@ func TestAuthorizationRoleAssignment(t *testing.T) { roleAssignment2 := &armauthorization.RoleAssignment{ Name: nil, // Role assignment with nil name should cause error Properties: &armauthorization.RoleAssignmentProperties{ - Scope: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg"), + Scope: new("/subscriptions/test-subscription/resourceGroups/test-rg"), }, } @@ -457,7 +456,7 @@ func TestAuthorizationRoleAssignment(t *testing.T) { wrapper := manual.NewAuthorizationRoleAssignment(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) // Use interface assertion to access PredefinedRole method - if roleInterface, ok := interface{}(wrapper).(interface{ PredefinedRole() string }); ok { + if roleInterface, ok := any(wrapper).(interface{ PredefinedRole() string }); ok { role := roleInterface.PredefinedRole() if role != "Reader" { t.Errorf("Expected PredefinedRole to be 'Reader', got %s", role) @@ -472,7 +471,7 @@ func TestAuthorizationRoleAssignment(t *testing.T) { roleAssignment := createAzureRoleAssignment(roleAssignmentName, "/subscriptions/test-subscription/resourceGroups/test-rg") // Add delegated managed identity resource ID delegatedIdentityID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" - roleAssignment.Properties.DelegatedManagedIdentityResourceID = to.Ptr(delegatedIdentityID) + roleAssignment.Properties.DelegatedManagedIdentityResourceID = new(delegatedIdentityID) mockClient := mocks.NewMockRoleAssignmentsClient(ctrl) azureScope := "/subscriptions/test-subscription/resourceGroups/test-rg" @@ -540,7 +539,7 @@ func (m *MockRoleAssignmentsPager) More() bool { func (mr *MockRoleAssignmentsPagerMockRecorder) More() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeOf((*MockRoleAssignmentsPager)(nil).More)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeFor[func() bool]()) } func (m *MockRoleAssignmentsPager) NextPage(ctx context.Context) (armauthorization.RoleAssignmentsClientListForResourceGroupResponse, error) { @@ -551,21 +550,21 @@ func (m *MockRoleAssignmentsPager) NextPage(ctx context.Context) (armauthorizati return ret0, ret1 } -func (mr *MockRoleAssignmentsPagerMockRecorder) NextPage(ctx interface{}) *gomock.Call { +func (mr *MockRoleAssignmentsPagerMockRecorder) NextPage(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeOf((*MockRoleAssignmentsPager)(nil).NextPage), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeFor[func(ctx context.Context) (armauthorization.RoleAssignmentsClientListForResourceGroupResponse, error)](), ctx) } // createAzureRoleAssignment creates a mock Azure role assignment for testing func createAzureRoleAssignment(roleAssignmentName, scope string) *armauthorization.RoleAssignment { return &armauthorization.RoleAssignment{ - Name: to.Ptr(roleAssignmentName), - Type: to.Ptr("Microsoft.Authorization/roleAssignments"), - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Authorization/roleAssignments/" + roleAssignmentName), + Name: new(roleAssignmentName), + Type: new("Microsoft.Authorization/roleAssignments"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Authorization/roleAssignments/" + roleAssignmentName), Properties: &armauthorization.RoleAssignmentProperties{ - Scope: to.Ptr(scope), - RoleDefinitionID: to.Ptr("/subscriptions/test-subscription/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"), - PrincipalID: to.Ptr("00000000-0000-0000-0000-000000000000"), + Scope: new(scope), + RoleDefinitionID: new("/subscriptions/test-subscription/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"), + PrincipalID: new("00000000-0000-0000-0000-000000000000"), }, } } diff --git a/sources/azure/manual/batch-batch-accounts_test.go b/sources/azure/manual/batch-batch-accounts_test.go index 8ecab3af..24143020 100644 --- a/sources/azure/manual/batch-batch-accounts_test.go +++ b/sources/azure/manual/batch-batch-accounts_test.go @@ -6,7 +6,6 @@ import ( "testing" "time" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" "go.uber.org/mock/gomock" @@ -446,37 +445,37 @@ func createAzureBatchAccount(accountName, provisioningState, subscriptionID, res nodeIdentityID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-node-identity" return &armbatch.Account{ - Name: to.Ptr(accountName), - Location: to.Ptr("eastus"), + Name: new(accountName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armbatch.AccountProperties{ - ProvisioningState: (*armbatch.ProvisioningState)(to.Ptr(provisioningState)), + ProvisioningState: (*armbatch.ProvisioningState)(new(provisioningState)), AutoStorage: &armbatch.AutoStorageProperties{ - StorageAccountID: to.Ptr(storageAccountID), - LastKeySync: to.Ptr(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), + StorageAccountID: new(storageAccountID), + LastKeySync: new(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), NodeIdentityReference: &armbatch.ComputeNodeIdentityReference{ - ResourceID: to.Ptr(nodeIdentityID), + ResourceID: new(nodeIdentityID), }, }, KeyVaultReference: &armbatch.KeyVaultReference{ - ID: to.Ptr(keyVaultID), - URL: to.Ptr("https://test-keyvault.vault.azure.net/"), + ID: new(keyVaultID), + URL: new("https://test-keyvault.vault.azure.net/"), }, PrivateEndpointConnections: []*armbatch.PrivateEndpointConnection{ { Properties: &armbatch.PrivateEndpointConnectionProperties{ PrivateEndpoint: &armbatch.PrivateEndpoint{ - ID: to.Ptr(privateEndpointID), + ID: new(privateEndpointID), }, }, }, }, }, Identity: &armbatch.AccountIdentity{ - Type: (*armbatch.ResourceIdentityType)(to.Ptr(armbatch.ResourceIdentityTypeUserAssigned)), + Type: (*armbatch.ResourceIdentityType)(new(armbatch.ResourceIdentityTypeUserAssigned)), UserAssignedIdentities: map[string]*armbatch.UserAssignedIdentities{ identityID: {}, }, @@ -494,16 +493,16 @@ func createAzureBatchAccountWithCrossRGResources( storageAccountID := "/subscriptions/" + otherSubscriptionID + "/resourceGroups/" + otherResourceGroup + "/providers/Microsoft.Storage/storageAccounts/test-storage-account" return &armbatch.Account{ - Name: to.Ptr(accountName), - Location: to.Ptr("eastus"), + Name: new(accountName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armbatch.AccountProperties{ - ProvisioningState: (*armbatch.ProvisioningState)(to.Ptr(provisioningState)), + ProvisioningState: (*armbatch.ProvisioningState)(new(provisioningState)), AutoStorage: &armbatch.AutoStorageProperties{ - StorageAccountID: to.Ptr(storageAccountID), - LastKeySync: to.Ptr(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), + StorageAccountID: new(storageAccountID), + LastKeySync: new(time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)), }, }, } diff --git a/sources/azure/manual/compute-availability-set_test.go b/sources/azure/manual/compute-availability-set_test.go index 12b22270..dfcae477 100644 --- a/sources/azure/manual/compute-availability-set_test.go +++ b/sources/azure/manual/compute-availability-set_test.go @@ -6,7 +6,6 @@ import ( "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" @@ -251,9 +250,9 @@ func TestComputeAvailabilitySet(t *testing.T) { avSet1 := createAzureAvailabilitySet("test-avset-1") avSetNilName := &armcompute.AvailabilitySet{ Name: nil, // nil name should be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } @@ -324,24 +323,24 @@ func TestComputeAvailabilitySet(t *testing.T) { // createAzureAvailabilitySet creates a mock Azure Availability Set for testing func createAzureAvailabilitySet(avSetName string) *armcompute.AvailabilitySet { return &armcompute.AvailabilitySet{ - Name: to.Ptr(avSetName), - Location: to.Ptr("eastus"), + Name: new(avSetName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcompute.AvailabilitySetProperties{ - PlatformFaultDomainCount: to.Ptr(int32(2)), - PlatformUpdateDomainCount: to.Ptr(int32(5)), + PlatformFaultDomainCount: new(int32(2)), + PlatformUpdateDomainCount: new(int32(5)), ProximityPlacementGroup: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/proximityPlacementGroups/test-ppg"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/proximityPlacementGroups/test-ppg"), }, VirtualMachines: []*armcompute.SubResource{ { - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines/test-vm-1"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines/test-vm-1"), }, { - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines/test-vm-2"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/virtualMachines/test-vm-2"), }, }, }, @@ -352,20 +351,20 @@ func createAzureAvailabilitySet(avSetName string) *armcompute.AvailabilitySet { // with links to resources in different resource groups func createAzureAvailabilitySetWithCrossResourceGroupLinks(avSetName, subscriptionID string) *armcompute.AvailabilitySet { return &armcompute.AvailabilitySet{ - Name: to.Ptr(avSetName), - Location: to.Ptr("eastus"), + Name: new(avSetName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.AvailabilitySetProperties{ - PlatformFaultDomainCount: to.Ptr(int32(2)), - PlatformUpdateDomainCount: to.Ptr(int32(5)), + PlatformFaultDomainCount: new(int32(2)), + PlatformUpdateDomainCount: new(int32(5)), ProximityPlacementGroup: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/other-rg/providers/Microsoft.Compute/proximityPlacementGroups/test-ppg"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/other-rg/providers/Microsoft.Compute/proximityPlacementGroups/test-ppg"), }, VirtualMachines: []*armcompute.SubResource{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/vm-rg/providers/Microsoft.Compute/virtualMachines/test-vm"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/vm-rg/providers/Microsoft.Compute/virtualMachines/test-vm"), }, }, }, @@ -375,14 +374,14 @@ func createAzureAvailabilitySetWithCrossResourceGroupLinks(avSetName, subscripti // createAzureAvailabilitySetWithoutLinks creates a mock Availability Set without any linked resources func createAzureAvailabilitySetWithoutLinks(avSetName string) *armcompute.AvailabilitySet { return &armcompute.AvailabilitySet{ - Name: to.Ptr(avSetName), - Location: to.Ptr("eastus"), + Name: new(avSetName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.AvailabilitySetProperties{ - PlatformFaultDomainCount: to.Ptr(int32(2)), - PlatformUpdateDomainCount: to.Ptr(int32(5)), + PlatformFaultDomainCount: new(int32(2)), + PlatformUpdateDomainCount: new(int32(5)), // No ProximityPlacementGroup // No VirtualMachines }, diff --git a/sources/azure/manual/compute-capacity-reservation-group_test.go b/sources/azure/manual/compute-capacity-reservation-group_test.go index fa22c505..6dfb123f 100644 --- a/sources/azure/manual/compute-capacity-reservation-group_test.go +++ b/sources/azure/manual/compute-capacity-reservation-group_test.go @@ -6,7 +6,6 @@ import ( "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" @@ -239,9 +238,9 @@ func TestComputeCapacityReservationGroup(t *testing.T) { crg1 := createAzureCapacityReservationGroup("test-crg-1") crgNilName := &armcompute.CapacityReservationGroup{ Name: nil, - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.CapacityReservationGroupProperties{}, } @@ -332,11 +331,11 @@ func capacityReservationGroupListOptions() *armcompute.CapacityReservationGroups // 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"), + Name: new(groupName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcompute.CapacityReservationGroupProperties{}, } @@ -347,20 +346,20 @@ func createAzureCapacityReservationGroupWithLinks(groupName, subscriptionID, res 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), + ID: new("/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), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/" + name), }) } return &armcompute.CapacityReservationGroup{ - Name: to.Ptr(groupName), - Location: to.Ptr("eastus"), + Name: new(groupName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.CapacityReservationGroupProperties{ CapacityReservations: reservations, diff --git a/sources/azure/manual/compute-dedicated-host-group_test.go b/sources/azure/manual/compute-dedicated-host-group_test.go index 5b24242f..605de8e2 100644 --- a/sources/azure/manual/compute-dedicated-host-group_test.go +++ b/sources/azure/manual/compute-dedicated-host-group_test.go @@ -6,7 +6,6 @@ import ( "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" @@ -225,12 +224,12 @@ func TestComputeDedicatedHostGroup(t *testing.T) { hostGroup1 := createAzureDedicatedHostGroup("test-host-group-1") hostGroupNilName := &armcompute.DedicatedHostGroup{ Name: nil, - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.DedicatedHostGroupProperties{ - PlatformFaultDomainCount: to.Ptr(int32(2)), + PlatformFaultDomainCount: new(int32(2)), }, } @@ -306,15 +305,15 @@ func TestComputeDedicatedHostGroup(t *testing.T) { // 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"), + Name: new(hostGroupName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcompute.DedicatedHostGroupProperties{ - PlatformFaultDomainCount: to.Ptr(int32(2)), - SupportAutomaticPlacement: to.Ptr(false), + PlatformFaultDomainCount: new(int32(2)), + SupportAutomaticPlacement: new(false), AdditionalCapabilities: nil, Hosts: nil, InstanceView: nil, @@ -327,17 +326,17 @@ func createAzureDedicatedHostGroupWithHosts(hostGroupName, subscriptionID, resou 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), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/hostGroups/" + hostGroupName + "/hosts/" + name), }) } return &armcompute.DedicatedHostGroup{ - Name: to.Ptr(hostGroupName), - Location: to.Ptr("eastus"), + Name: new(hostGroupName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.DedicatedHostGroupProperties{ - PlatformFaultDomainCount: to.Ptr(int32(2)), + PlatformFaultDomainCount: new(int32(2)), Hosts: hosts, }, } diff --git a/sources/azure/manual/compute-disk-access_test.go b/sources/azure/manual/compute-disk-access_test.go index 9b042210..128f0c90 100644 --- a/sources/azure/manual/compute-disk-access_test.go +++ b/sources/azure/manual/compute-disk-access_test.go @@ -6,7 +6,6 @@ import ( "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" @@ -239,9 +238,9 @@ func TestComputeDiskAccess(t *testing.T) { diskAccess1 := createAzureDiskAccess("test-disk-access-1") diskAccessNilName := &armcompute.DiskAccess{ Name: nil, - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } @@ -317,14 +316,14 @@ func TestComputeDiskAccess(t *testing.T) { // 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"), + Name: new(diskAccessName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcompute.DiskAccessProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), }, } } @@ -332,27 +331,27 @@ func createAzureDiskAccess(diskAccessName string) *armcompute.DiskAccess { // 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"), + Name: new(diskAccessName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.DiskAccessProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), PrivateEndpointConnections: []*armcompute.PrivateEndpointConnection{ { - Name: to.Ptr("pe-connection-1"), + Name: new("pe-connection-1"), Properties: &armcompute.PrivateEndpointConnectionProperties{ PrivateEndpoint: &armcompute.PrivateEndpoint{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-private-endpoint"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-private-endpoint"), }, }, }, { - Name: to.Ptr("pe-connection-2"), + Name: new("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"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/other-rg/providers/Microsoft.Network/privateEndpoints/test-private-endpoint-other-rg"), }, }, }, diff --git a/sources/azure/manual/compute-disk-encryption-set_test.go b/sources/azure/manual/compute-disk-encryption-set_test.go index 2052400f..f166ce83 100644 --- a/sources/azure/manual/compute-disk-encryption-set_test.go +++ b/sources/azure/manual/compute-disk-encryption-set_test.go @@ -7,7 +7,6 @@ import ( "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" @@ -264,7 +263,7 @@ func TestComputeDiskEncryptionSet(t *testing.T) { desName := "test-des" des := &armcompute.DiskEncryptionSet{ Name: nil, - Location: to.Ptr("eastus"), + Location: new("eastus"), } mockClient := mocks.NewMockDiskEncryptionSetsClient(ctrl) @@ -311,7 +310,7 @@ func TestComputeDiskEncryptionSet(t *testing.T) { des1 := createAzureDiskEncryptionSet("test-des-1") desNil := &armcompute.DiskEncryptionSet{ Name: nil, // Should be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), } mockClient := mocks.NewMockDiskEncryptionSetsClient(ctrl) @@ -430,30 +429,30 @@ func TestComputeDiskEncryptionSet(t *testing.T) { func createAzureDiskEncryptionSet(name string) *armcompute.DiskEncryptionSet { return &armcompute.DiskEncryptionSet{ - Name: to.Ptr(name), - Location: to.Ptr("eastus"), + Name: new(name), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.EncryptionSetProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), }, } } func createAzureDiskEncryptionSetWithAllLinks(name, subscriptionID, resourceGroup string) *armcompute.DiskEncryptionSet { return &armcompute.DiskEncryptionSet{ - Name: to.Ptr(name), - Location: to.Ptr("eastus"), + Name: new(name), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.EncryptionSetProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), ActiveKey: &armcompute.KeyForDiskEncryptionSet{ - KeyURL: to.Ptr("https://test-vault.vault.azure.net/keys/test-key/00000000000000000000000000000000"), + KeyURL: new("https://test-vault.vault.azure.net/keys/test-key/00000000000000000000000000000000"), SourceVault: &armcompute.SourceVault{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-vault"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-vault"), }, }, }, @@ -469,9 +468,9 @@ func createAzureDiskEncryptionSetWithPreviousKeys(name, subscriptionID, resource des := createAzureDiskEncryptionSetWithAllLinks(name, subscriptionID, resourceGroup) des.Properties.PreviousKeys = []*armcompute.KeyForDiskEncryptionSet{ { - KeyURL: to.Ptr("https://test-old-vault.vault.azure.net/keys/test-old-key/00000000000000000000000000000000"), + KeyURL: new("https://test-old-vault.vault.azure.net/keys/test-old-key/00000000000000000000000000000000"), SourceVault: &armcompute.SourceVault{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-old-vault"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-old-vault"), }, }, } @@ -483,9 +482,9 @@ func createAzureDiskEncryptionSetWithPreviousKeysSameVault(name, subscriptionID, des.Properties.PreviousKeys = []*armcompute.KeyForDiskEncryptionSet{ { // Same vault + key as ActiveKey.KeyURL to ensure links are deduplicated. - KeyURL: to.Ptr("https://test-vault.vault.azure.net/keys/test-key/00000000000000000000000000000000"), + KeyURL: new("https://test-vault.vault.azure.net/keys/test-key/00000000000000000000000000000000"), SourceVault: &armcompute.SourceVault{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-vault"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-vault"), }, }, } diff --git a/sources/azure/manual/compute-disk_test.go b/sources/azure/manual/compute-disk_test.go index 8dd2c7f9..f503c364 100644 --- a/sources/azure/manual/compute-disk_test.go +++ b/sources/azure/manual/compute-disk_test.go @@ -6,7 +6,6 @@ import ( "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" @@ -365,9 +364,9 @@ func TestComputeDisk(t *testing.T) { disk1 := createAzureDisk("test-disk-1", "Succeeded") diskNilName := &armcompute.Disk{ Name: nil, // nil name should be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } @@ -489,17 +488,17 @@ func TestComputeDisk(t *testing.T) { // createAzureDisk creates a mock Azure Disk for testing func createAzureDisk(diskName, provisioningState string) *armcompute.Disk { return &armcompute.Disk{ - Name: to.Ptr(diskName), - Location: to.Ptr("eastus"), + Name: new(diskName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcompute.DiskProperties{ - ProvisioningState: to.Ptr(provisioningState), - DiskSizeGB: to.Ptr(int32(128)), + ProvisioningState: new(provisioningState), + DiskSizeGB: new(int32(128)), CreationData: &armcompute.CreationData{ - CreateOption: to.Ptr(armcompute.DiskCreateOptionEmpty), + CreateOption: new(armcompute.DiskCreateOptionEmpty), }, }, } @@ -508,59 +507,59 @@ func createAzureDisk(diskName, provisioningState string) *armcompute.Disk { // createAzureDiskWithAllLinks creates a mock Azure Disk with all possible linked resources func createAzureDiskWithAllLinks(diskName, subscriptionID, resourceGroup string) *armcompute.Disk { return &armcompute.Disk{ - Name: to.Ptr(diskName), - Location: to.Ptr("eastus"), + Name: new(diskName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, - ManagedBy: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/test-vm"), + ManagedBy: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/test-vm"), ManagedByExtended: []*string{ - to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/test-vm-2"), + new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/test-vm-2"), }, Properties: &armcompute.DiskProperties{ - ProvisioningState: to.Ptr("Succeeded"), - DiskSizeGB: to.Ptr(int32(128)), - DiskAccessID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskAccesses/test-disk-access"), + ProvisioningState: new("Succeeded"), + DiskSizeGB: new(int32(128)), + DiskAccessID: new("/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-disk-encryption-set"), + DiskEncryptionSetID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskEncryptionSets/test-disk-encryption-set"), }, SecurityProfile: &armcompute.DiskSecurityProfile{ - SecureVMDiskEncryptionSetID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskEncryptionSets/test-secure-vm-disk-encryption-set"), + SecureVMDiskEncryptionSetID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskEncryptionSets/test-secure-vm-disk-encryption-set"), }, ShareInfo: []*armcompute.ShareInfoElement{ { - VMURI: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/test-vm-3"), + VMURI: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/test-vm-3"), }, }, CreationData: &armcompute.CreationData{ - CreateOption: to.Ptr(armcompute.DiskCreateOptionCopy), - SourceResourceID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/disks/source-disk"), - StorageAccountID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Storage/storageAccounts/test-storage-account"), + CreateOption: new(armcompute.DiskCreateOptionCopy), + SourceResourceID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/disks/source-disk"), + StorageAccountID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Storage/storageAccounts/test-storage-account"), ImageReference: &armcompute.ImageDiskReference{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/images/test-image"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/images/test-image"), }, GalleryImageReference: &armcompute.ImageDiskReference{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/galleries/test-gallery/images/test-gallery-image/versions/1.0.0"), - SharedGalleryImageID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/galleries/test-gallery-2/images/test-gallery-image-2/versions/2.0.0"), - CommunityGalleryImageID: to.Ptr("/CommunityGalleries/test-community-gallery/Images/test-community-image/Versions/1.0.0"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/galleries/test-gallery/images/test-gallery-image/versions/1.0.0"), + SharedGalleryImageID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/galleries/test-gallery-2/images/test-gallery-image-2/versions/2.0.0"), + CommunityGalleryImageID: new("/CommunityGalleries/test-community-gallery/Images/test-community-image/Versions/1.0.0"), }, - ElasticSanResourceID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.ElasticSan/elasticSans/test-elastic-san/volumegroups/test-volume-group/snapshots/test-snapshot"), + ElasticSanResourceID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.ElasticSan/elasticSans/test-elastic-san/volumegroups/test-volume-group/snapshots/test-snapshot"), }, EncryptionSettingsCollection: &armcompute.EncryptionSettingsCollection{ - Enabled: to.Ptr(true), + Enabled: new(true), EncryptionSettings: []*armcompute.EncryptionSettingsElement{ { DiskEncryptionKey: &armcompute.KeyVaultAndSecretReference{ SourceVault: &armcompute.SourceVault{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-keyvault"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-keyvault"), }, - SecretURL: to.Ptr("https://test-keyvault.vault.azure.net/secrets/test-secret/version"), + SecretURL: new("https://test-keyvault.vault.azure.net/secrets/test-secret/version"), }, KeyEncryptionKey: &armcompute.KeyVaultAndKeyReference{ SourceVault: &armcompute.SourceVault{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-keyvault-2"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-keyvault-2"), }, - KeyURL: to.Ptr("https://test-keyvault-2.vault.azure.net/keys/test-key/version"), + KeyURL: new("https://test-keyvault-2.vault.azure.net/keys/test-key/version"), }, }, }, @@ -572,17 +571,17 @@ func createAzureDiskWithAllLinks(diskName, subscriptionID, resourceGroup string) // createAzureDiskFromSnapshot creates a mock Azure Disk created from a snapshot func createAzureDiskFromSnapshot(diskName, subscriptionID, resourceGroup string) *armcompute.Disk { return &armcompute.Disk{ - Name: to.Ptr(diskName), - Location: to.Ptr("eastus"), + Name: new(diskName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.DiskProperties{ - ProvisioningState: to.Ptr("Succeeded"), - DiskSizeGB: to.Ptr(int32(128)), + ProvisioningState: new("Succeeded"), + DiskSizeGB: new(int32(128)), CreationData: &armcompute.CreationData{ - CreateOption: to.Ptr(armcompute.DiskCreateOptionCopy), - SourceResourceID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/snapshots/test-snapshot"), + CreateOption: new(armcompute.DiskCreateOptionCopy), + SourceResourceID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/snapshots/test-snapshot"), }, }, } @@ -591,17 +590,17 @@ func createAzureDiskFromSnapshot(diskName, subscriptionID, resourceGroup string) // createAzureDiskWithCrossResourceGroupLinks creates a mock Azure Disk with links to resources in different resource groups func createAzureDiskWithCrossResourceGroupLinks(diskName, subscriptionID, resourceGroup string) *armcompute.Disk { return &armcompute.Disk{ - Name: to.Ptr(diskName), - Location: to.Ptr("eastus"), + Name: new(diskName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, - ManagedBy: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/other-rg/providers/Microsoft.Compute/virtualMachines/test-vm-other-rg"), + ManagedBy: new("/subscriptions/" + subscriptionID + "/resourceGroups/other-rg/providers/Microsoft.Compute/virtualMachines/test-vm-other-rg"), Properties: &armcompute.DiskProperties{ - ProvisioningState: to.Ptr("Succeeded"), - DiskSizeGB: to.Ptr(int32(128)), + ProvisioningState: new("Succeeded"), + DiskSizeGB: new(int32(128)), CreationData: &armcompute.CreationData{ - CreateOption: to.Ptr(armcompute.DiskCreateOptionEmpty), + CreateOption: new(armcompute.DiskCreateOptionEmpty), }, }, } diff --git a/sources/azure/manual/compute-gallery-application-version_test.go b/sources/azure/manual/compute-gallery-application-version_test.go index 25716e24..37e262b1 100644 --- a/sources/azure/manual/compute-gallery-application-version_test.go +++ b/sources/azure/manual/compute-gallery-application-version_test.go @@ -5,7 +5,6 @@ import ( "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" @@ -65,15 +64,15 @@ func (t *testGalleryApplicationVersionsClient) NewListByGalleryApplicationPager( func createAzureGalleryApplicationVersion(versionName string) *armcompute.GalleryApplicationVersion { return &armcompute.GalleryApplicationVersion{ - Name: to.Ptr(versionName), - Location: to.Ptr("eastus"), + Name: new(versionName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.GalleryApplicationVersionProperties{ PublishingProfile: &armcompute.GalleryApplicationVersionPublishingProfile{ Source: &armcompute.UserArtifactSource{ - MediaLink: to.Ptr("https://mystorageaccount.blob.core.windows.net/packages/app.zip"), + MediaLink: new("https://mystorageaccount.blob.core.windows.net/packages/app.zip"), }, }, }, @@ -82,14 +81,14 @@ func createAzureGalleryApplicationVersion(versionName string) *armcompute.Galler 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") + v.Properties.PublishingProfile.Source.DefaultConfigurationLink = new("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"), + Name: new("eastus"), Encryption: &armcompute.EncryptionImages{ OSDiskImage: &armcompute.OSDiskImageEncryption{ - DiskEncryptionSetID: to.Ptr(desID), + DiskEncryptionSetID: new(desID), }, }, }, @@ -234,7 +233,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { 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") + version.Properties.PublishingProfile.Source.MediaLink = new("https://example.com/artifacts/app.zip") mockClient := NewMockGalleryApplicationVersionsClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( @@ -286,7 +285,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { 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") + version.Properties.PublishingProfile.Source.MediaLink = new("https://192.168.1.10:8443/artifacts/app.zip") mockClient := NewMockGalleryApplicationVersionsClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( diff --git a/sources/azure/manual/compute-gallery-application_test.go b/sources/azure/manual/compute-gallery-application_test.go index c1ce85a4..57b35206 100644 --- a/sources/azure/manual/compute-gallery-application_test.go +++ b/sources/azure/manual/compute-gallery-application_test.go @@ -64,14 +64,14 @@ func (t *testGalleryApplicationsClient) NewListByGalleryPager(resourceGroupName, func createAzureGalleryApplication(applicationName string) *armcompute.GalleryApplication { return &armcompute.GalleryApplication{ - Name: to.Ptr(applicationName), - Location: to.Ptr("eastus"), + Name: new(applicationName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.GalleryApplicationProperties{ SupportedOSType: to.Ptr(armcompute.OperatingSystemTypesWindows), - Description: to.Ptr("Test gallery application"), + Description: new("Test gallery application"), }, } } diff --git a/sources/azure/manual/compute-gallery-image_test.go b/sources/azure/manual/compute-gallery-image_test.go index 5a2cd299..57d4e159 100644 --- a/sources/azure/manual/compute-gallery-image_test.go +++ b/sources/azure/manual/compute-gallery-image_test.go @@ -5,7 +5,6 @@ import ( "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" @@ -65,28 +64,28 @@ func (t *testGalleryImagesClient) NewListByGalleryPager(resourceGroupName, galle func createAzureGalleryImage(imageName string) *armcompute.GalleryImage { return &armcompute.GalleryImage{ - Name: to.Ptr(imageName), - Location: to.Ptr("eastus"), + Name: new(imageName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.GalleryImageProperties{ Identifier: &armcompute.GalleryImageIdentifier{ - Publisher: to.Ptr("test-publisher"), - Offer: to.Ptr("test-offer"), - SKU: to.Ptr("test-sku"), + Publisher: new("test-publisher"), + Offer: new("test-offer"), + SKU: new("test-sku"), }, - OSType: to.Ptr(armcompute.OperatingSystemTypesLinux), - OSState: to.Ptr(armcompute.OperatingSystemStateTypesGeneralized), + OSType: new(armcompute.OperatingSystemTypesLinux), + OSState: new(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") + img.Properties.Eula = new("https://eula.example.com/terms") + img.Properties.PrivacyStatementURI = new("https://example.com/privacy") + img.Properties.ReleaseNoteURI = new("https://releases.example.com/notes") return img } @@ -178,7 +177,7 @@ func TestComputeGalleryImage(t *testing.T) { 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.") + image.Properties.Eula = new("This software is provided as-is. No warranty.") mockClient := NewMockGalleryImagesClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( @@ -210,8 +209,8 @@ func TestComputeGalleryImage(t *testing.T) { 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") + image.Properties.PrivacyStatementURI = new("https://example.com/privacy") + image.Properties.ReleaseNoteURI = new("https://example.com/release-notes") mockClient := NewMockGalleryImagesClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( @@ -252,7 +251,7 @@ func TestComputeGalleryImage(t *testing.T) { t.Run("Get_IPHost_EmitsIPLink", func(t *testing.T) { image := createAzureGalleryImage(galleryImageName) - image.Properties.PrivacyStatementURI = to.Ptr("https://192.168.1.10:8443/privacy") + image.Properties.PrivacyStatementURI = new("https://192.168.1.10:8443/privacy") mockClient := NewMockGalleryImagesClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( diff --git a/sources/azure/manual/compute-gallery_test.go b/sources/azure/manual/compute-gallery_test.go index 4ba09556..0a3ec302 100644 --- a/sources/azure/manual/compute-gallery_test.go +++ b/sources/azure/manual/compute-gallery_test.go @@ -6,7 +6,6 @@ import ( "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" @@ -165,9 +164,9 @@ func TestComputeGallery(t *testing.T) { gallery1 := createAzureGallery("test-gallery-1") galleryNilName := &armcompute.Gallery{ Name: nil, - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } @@ -235,18 +234,18 @@ func TestComputeGallery(t *testing.T) { func createAzureGallery(galleryName string) *armcompute.Gallery { return &armcompute.Gallery{ - Name: to.Ptr(galleryName), - Location: to.Ptr("eastus"), + Name: new(galleryName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcompute.GalleryProperties{ - Description: to.Ptr("Test shared image gallery"), + Description: new("Test shared image gallery"), Identifier: &armcompute.GalleryIdentifier{ - UniqueName: to.Ptr("unique-" + galleryName), + UniqueName: new("unique-" + galleryName), }, - ProvisioningState: to.Ptr(armcompute.GalleryProvisioningStateSucceeded), + ProvisioningState: new(armcompute.GalleryProvisioningStateSucceeded), }, } } diff --git a/sources/azure/manual/compute-image_test.go b/sources/azure/manual/compute-image_test.go index dd3a0609..475248a6 100644 --- a/sources/azure/manual/compute-image_test.go +++ b/sources/azure/manual/compute-image_test.go @@ -6,7 +6,6 @@ import ( "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" @@ -298,9 +297,9 @@ func TestComputeImage(t *testing.T) { image1 := createAzureImage("test-image-1") imageNilName := &armcompute.Image{ Name: nil, // nil name should be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } @@ -483,7 +482,7 @@ func TestComputeImage(t *testing.T) { wrapper := manual.NewComputeImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) // PredefinedRole is available on the wrapper, not the adapter - if roleInterface, ok := interface{}(wrapper).(interface{ PredefinedRole() string }); ok { + if roleInterface, ok := any(wrapper).(interface{ PredefinedRole() string }); ok { role := roleInterface.PredefinedRole() if role != "Reader" { t.Errorf("Expected predefined role 'Reader', got %s", role) @@ -497,14 +496,14 @@ func TestComputeImage(t *testing.T) { // createAzureImage creates a mock Azure Image for testing func createAzureImage(imageName string) *armcompute.Image { return &armcompute.Image{ - Name: to.Ptr(imageName), - Location: to.Ptr("eastus"), + Name: new(imageName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcompute.ImageProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), }, } } @@ -515,46 +514,46 @@ func createAzureImageWithAllLinks(imageName, subscriptionID, resourceGroup strin dataDiskBlobURI := "https://teststorageaccount2.blob.core.windows.net/vhds/datadisk1.vhd" return &armcompute.Image{ - Name: to.Ptr(imageName), - Location: to.Ptr("eastus"), + Name: new(imageName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.ImageProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), StorageProfile: &armcompute.ImageStorageProfile{ OSDisk: &armcompute.ImageOSDisk{ - OSType: to.Ptr(armcompute.OperatingSystemTypesLinux), - OSState: to.Ptr(armcompute.OperatingSystemStateTypesGeneralized), + OSType: new(armcompute.OperatingSystemTypesLinux), + OSState: new(armcompute.OperatingSystemStateTypesGeneralized), ManagedDisk: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/disks/test-os-disk"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/disks/test-os-disk"), }, Snapshot: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/snapshots/test-os-snapshot"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/snapshots/test-os-snapshot"), }, - BlobURI: to.Ptr(osDiskBlobURI), + BlobURI: new(osDiskBlobURI), DiskEncryptionSet: &armcompute.DiskEncryptionSetParameters{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskEncryptionSets/test-os-disk-encryption-set"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskEncryptionSets/test-os-disk-encryption-set"), }, }, DataDisks: []*armcompute.ImageDataDisk{ { - Lun: to.Ptr(int32(0)), + Lun: new(int32(0)), ManagedDisk: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/disks/test-data-disk-1"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/disks/test-data-disk-1"), }, Snapshot: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/snapshots/test-data-snapshot-1"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/snapshots/test-data-snapshot-1"), }, - BlobURI: to.Ptr(dataDiskBlobURI), + BlobURI: new(dataDiskBlobURI), DiskEncryptionSet: &armcompute.DiskEncryptionSetParameters{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskEncryptionSets/test-data-disk-encryption-set"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskEncryptionSets/test-data-disk-encryption-set"), }, }, }, }, SourceVirtualMachine: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/test-source-vm"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/test-source-vm"), }, }, } @@ -563,19 +562,19 @@ func createAzureImageWithAllLinks(imageName, subscriptionID, resourceGroup strin // createAzureImageWithCrossResourceGroupLinks creates a mock Azure Image with links to resources in different resource groups func createAzureImageWithCrossResourceGroupLinks(imageName, subscriptionID, resourceGroup string) *armcompute.Image { return &armcompute.Image{ - Name: to.Ptr(imageName), - Location: to.Ptr("eastus"), + Name: new(imageName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.ImageProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), StorageProfile: &armcompute.ImageStorageProfile{ OSDisk: &armcompute.ImageOSDisk{ - OSType: to.Ptr(armcompute.OperatingSystemTypesLinux), - OSState: to.Ptr(armcompute.OperatingSystemStateTypesGeneralized), + OSType: new(armcompute.OperatingSystemTypesLinux), + OSState: new(armcompute.OperatingSystemStateTypesGeneralized), ManagedDisk: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/other-rg/providers/Microsoft.Compute/disks/test-disk-other-rg"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/other-rg/providers/Microsoft.Compute/disks/test-disk-other-rg"), }, }, }, diff --git a/sources/azure/manual/compute-proximity-placement-group_test.go b/sources/azure/manual/compute-proximity-placement-group_test.go index 7e36dd32..464a8d82 100644 --- a/sources/azure/manual/compute-proximity-placement-group_test.go +++ b/sources/azure/manual/compute-proximity-placement-group_test.go @@ -5,7 +5,6 @@ import ( "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" @@ -194,9 +193,9 @@ func TestComputeProximityPlacementGroup(t *testing.T) { ppg1 := createAzureProximityPlacementGroup("test-ppg-1", subscriptionID, resourceGroup) ppgNilName := &armcompute.ProximityPlacementGroup{ Name: nil, - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } @@ -258,45 +257,45 @@ func TestComputeProximityPlacementGroup(t *testing.T) { func createAzureProximityPlacementGroup(ppgName, subscriptionID, resourceGroup string) *armcompute.ProximityPlacementGroup { baseID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute" return &armcompute.ProximityPlacementGroup{ - Name: to.Ptr(ppgName), - Location: to.Ptr("eastus"), + Name: new(ppgName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcompute.ProximityPlacementGroupProperties{ - ProximityPlacementGroupType: to.Ptr(armcompute.ProximityPlacementGroupTypeStandard), + ProximityPlacementGroupType: new(armcompute.ProximityPlacementGroupTypeStandard), VirtualMachines: []*armcompute.SubResourceWithColocationStatus{ - {ID: to.Ptr(baseID + "/virtualMachines/test-vm")}, + {ID: new(baseID + "/virtualMachines/test-vm")}, }, AvailabilitySets: []*armcompute.SubResourceWithColocationStatus{ - {ID: to.Ptr(baseID + "/availabilitySets/test-avset")}, + {ID: new(baseID + "/availabilitySets/test-avset")}, }, VirtualMachineScaleSets: []*armcompute.SubResourceWithColocationStatus{ - {ID: to.Ptr(baseID + "/virtualMachineScaleSets/test-vmss")}, + {ID: new(baseID + "/virtualMachineScaleSets/test-vmss")}, }, }, - Zones: []*string{to.Ptr("1")}, + Zones: []*string{new("1")}, } } func createAzureProximityPlacementGroupWithCrossResourceGroupLinks(ppgName, subscriptionID string) *armcompute.ProximityPlacementGroup { return &armcompute.ProximityPlacementGroup{ - Name: to.Ptr(ppgName), - Location: to.Ptr("eastus"), + Name: new(ppgName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.ProximityPlacementGroupProperties{ - ProximityPlacementGroupType: to.Ptr(armcompute.ProximityPlacementGroupTypeStandard), + ProximityPlacementGroupType: new(armcompute.ProximityPlacementGroupTypeStandard), VirtualMachines: []*armcompute.SubResourceWithColocationStatus{ - {ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/vm-rg/providers/Microsoft.Compute/virtualMachines/test-vm")}, + {ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/vm-rg/providers/Microsoft.Compute/virtualMachines/test-vm")}, }, AvailabilitySets: []*armcompute.SubResourceWithColocationStatus{ - {ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/avset-rg/providers/Microsoft.Compute/availabilitySets/test-avset")}, + {ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/avset-rg/providers/Microsoft.Compute/availabilitySets/test-avset")}, }, VirtualMachineScaleSets: []*armcompute.SubResourceWithColocationStatus{ - {ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/vmss-rg/providers/Microsoft.Compute/virtualMachineScaleSets/test-vmss")}, + {ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/vmss-rg/providers/Microsoft.Compute/virtualMachineScaleSets/test-vmss")}, }, }, } @@ -304,13 +303,13 @@ func createAzureProximityPlacementGroupWithCrossResourceGroupLinks(ppgName, subs func createAzureProximityPlacementGroupWithoutLinks(ppgName string) *armcompute.ProximityPlacementGroup { return &armcompute.ProximityPlacementGroup{ - Name: to.Ptr(ppgName), - Location: to.Ptr("eastus"), + Name: new(ppgName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.ProximityPlacementGroupProperties{ - ProximityPlacementGroupType: to.Ptr(armcompute.ProximityPlacementGroupTypeStandard), + ProximityPlacementGroupType: new(armcompute.ProximityPlacementGroupTypeStandard), }, } } diff --git a/sources/azure/manual/compute-shared-gallery-image_test.go b/sources/azure/manual/compute-shared-gallery-image_test.go index 25efb0a7..bff2bdd2 100644 --- a/sources/azure/manual/compute-shared-gallery-image_test.go +++ b/sources/azure/manual/compute-shared-gallery-image_test.go @@ -5,7 +5,6 @@ import ( "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" @@ -102,7 +101,7 @@ func TestComputeSharedGalleryImage(t *testing.T) { 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.") + image.Properties.Eula = new("This software is provided as-is. No warranty.") mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) mockClient.EXPECT().Get(ctx, location, galleryUniqueName, imageName, nil).Return( @@ -133,8 +132,8 @@ func TestComputeSharedGalleryImage(t *testing.T) { 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") + image.Properties.Eula = new("https://example.com/eula") + image.Properties.PrivacyStatementURI = new("https://example.com/privacy") mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) mockClient.EXPECT().Get(ctx, location, galleryUniqueName, imageName, nil).Return( @@ -174,7 +173,7 @@ func TestComputeSharedGalleryImage(t *testing.T) { t.Run("Get_IPHost_EmitsIPLink", func(t *testing.T) { image := createSharedGalleryImage(imageName) - image.Properties.PrivacyStatementURI = to.Ptr("https://192.168.1.10:8443/privacy") + image.Properties.PrivacyStatementURI = new("https://192.168.1.10:8443/privacy") mockClient := mocks.NewMockSharedGalleryImagesClient(ctrl) mockClient.EXPECT().Get(ctx, location, galleryUniqueName, imageName, nil).Return( @@ -398,27 +397,27 @@ func TestComputeSharedGalleryImage(t *testing.T) { func createSharedGalleryImage(name string) *armcompute.SharedGalleryImage { return &armcompute.SharedGalleryImage{ - Name: to.Ptr(name), - Location: to.Ptr("eastus"), + Name: new(name), + Location: new("eastus"), Identifier: &armcompute.SharedGalleryIdentifier{ - UniqueID: to.Ptr("/SharedGalleries/test-gallery-unique-name"), + UniqueID: new("/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"), + Publisher: new("test-publisher"), + Offer: new("test-offer"), + SKU: new("test-sku"), }, - OSType: to.Ptr(armcompute.OperatingSystemTypesLinux), - OSState: to.Ptr(armcompute.OperatingSystemStateTypesGeneralized), + OSType: new(armcompute.OperatingSystemTypesLinux), + OSState: new(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") + img.Properties.Eula = new("https://eula.example.com/terms") + img.Properties.PrivacyStatementURI = new("https://example.com/privacy") return img } diff --git a/sources/azure/manual/compute-snapshot_test.go b/sources/azure/manual/compute-snapshot_test.go index db9a434a..3208b2f1 100644 --- a/sources/azure/manual/compute-snapshot_test.go +++ b/sources/azure/manual/compute-snapshot_test.go @@ -6,7 +6,6 @@ import ( "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" @@ -75,19 +74,19 @@ func TestComputeSnapshot(t *testing.T) { ExpectedType: azureshared.ComputeDiskAccess.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-disk-access", - ExpectedScope: subscriptionID + "." + resourceGroup, }, + ExpectedScope: subscriptionID + "." + resourceGroup}, { // Properties.Encryption.DiskEncryptionSetID ExpectedType: azureshared.ComputeDiskEncryptionSet.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "test-des", - ExpectedScope: subscriptionID + "." + resourceGroup, }, + ExpectedScope: subscriptionID + "." + resourceGroup}, { // Properties.CreationData.SourceResourceID (disk) ExpectedType: azureshared.ComputeDisk.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-disk", - ExpectedScope: subscriptionID + "." + resourceGroup, }, + ExpectedScope: subscriptionID + "." + resourceGroup}, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -119,7 +118,7 @@ func TestComputeSnapshot(t *testing.T) { ExpectedType: azureshared.ComputeSnapshot.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "source-snapshot", - ExpectedScope: subscriptionID + "." + resourceGroup, }, + ExpectedScope: subscriptionID + "." + resourceGroup}, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -151,25 +150,25 @@ func TestComputeSnapshot(t *testing.T) { ExpectedType: azureshared.StorageAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "teststorageaccount", - ExpectedScope: subscriptionID + "." + resourceGroup, }, + ExpectedScope: subscriptionID + "." + resourceGroup}, { // Properties.CreationData.SourceURI → Blob Container ExpectedType: azureshared.StorageBlobContainer.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: shared.CompositeLookupKey("teststorageaccount", "vhds"), - ExpectedScope: subscriptionID + "." + resourceGroup, }, + 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", }, + ExpectedScope: "global"}, { // Properties.CreationData.SourceURI → DNS ExpectedType: stdlib.NetworkDNS.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: "teststorageaccount.blob.core.windows.net", - ExpectedScope: "global", }, + ExpectedScope: "global"}, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -201,13 +200,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", }, + 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", }, + ExpectedScope: "global"}, } shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -429,9 +428,9 @@ func TestComputeSnapshot(t *testing.T) { snapshot1 := createAzureSnapshot("test-snapshot-1", subscriptionID, resourceGroup) snapshotNilName := &armcompute.Snapshot{ Name: nil, - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } @@ -500,21 +499,21 @@ func TestComputeSnapshot(t *testing.T) { // 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"), + Name: new(name), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcompute.SnapshotProperties{ - ProvisioningState: to.Ptr("Succeeded"), - DiskAccessID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/diskAccesses/test-disk-access"), + ProvisioningState: new("Succeeded"), + DiskAccessID: new("/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"), + DiskEncryptionSetID: new("/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"), + CreateOption: new(armcompute.DiskCreateOptionCopy), + SourceResourceID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/disks/source-disk"), }, }, } @@ -523,16 +522,16 @@ func createAzureSnapshot(name, subscriptionID, resourceGroup string) *armcompute // 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"), + Name: new(name), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.SnapshotProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), CreationData: &armcompute.CreationData{ - CreateOption: to.Ptr(armcompute.DiskCreateOptionCopy), - SourceResourceID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/snapshots/source-snapshot"), + CreateOption: new(armcompute.DiskCreateOptionCopy), + SourceResourceID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/snapshots/source-snapshot"), }, }, } @@ -541,16 +540,16 @@ func createAzureSnapshotFromSnapshot(name, subscriptionID, resourceGroup string) // 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"), + Name: new(name), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.SnapshotProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), CreationData: &armcompute.CreationData{ - CreateOption: to.Ptr(armcompute.DiskCreateOptionImport), - SourceURI: to.Ptr("https://teststorageaccount.blob.core.windows.net/vhds/my-disk.vhd"), + CreateOption: new(armcompute.DiskCreateOptionImport), + SourceURI: new("https://teststorageaccount.blob.core.windows.net/vhds/my-disk.vhd"), }, }, } @@ -559,16 +558,16 @@ func createAzureSnapshotFromBlobURI(name string) *armcompute.Snapshot { // 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"), + Name: new(name), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.SnapshotProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), CreationData: &armcompute.CreationData{ - CreateOption: to.Ptr(armcompute.DiskCreateOptionImport), - SourceURI: to.Ptr("https://10.0.0.1/vhds/my-disk.vhd"), + CreateOption: new(armcompute.DiskCreateOptionImport), + SourceURI: new("https://10.0.0.1/vhds/my-disk.vhd"), }, }, } @@ -577,31 +576,31 @@ func createAzureSnapshotFromIPBlobURI(name string) *armcompute.Snapshot { // 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"), + Name: new(name), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.SnapshotProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), CreationData: &armcompute.CreationData{ - CreateOption: to.Ptr(armcompute.DiskCreateOptionEmpty), + CreateOption: new(armcompute.DiskCreateOptionEmpty), }, EncryptionSettingsCollection: &armcompute.EncryptionSettingsCollection{ - Enabled: to.Ptr(true), + Enabled: new(true), EncryptionSettings: []*armcompute.EncryptionSettingsElement{ { DiskEncryptionKey: &armcompute.KeyVaultAndSecretReference{ SourceVault: &armcompute.SourceVault{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-vault"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-vault"), }, - SecretURL: to.Ptr("https://10.0.0.2/secrets/my-secret/version1"), + SecretURL: new("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"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/vaults/test-vault"), }, - KeyURL: to.Ptr("https://10.0.0.3/keys/my-key/version1"), + KeyURL: new("https://10.0.0.3/keys/my-key/version1"), }, }, }, @@ -613,17 +612,17 @@ func createAzureSnapshotWithEncryptionIPHosts(name, subscriptionID, resourceGrou // 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"), + Name: new(name), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.SnapshotProperties{ - ProvisioningState: to.Ptr("Succeeded"), - DiskAccessID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/other-rg/providers/Microsoft.Compute/diskAccesses/test-disk-access"), + ProvisioningState: new("Succeeded"), + DiskAccessID: new("/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"), + CreateOption: new(armcompute.DiskCreateOptionCopy), + SourceResourceID: new("/subscriptions/" + subscriptionID + "/resourceGroups/disk-rg/providers/Microsoft.Compute/disks/source-disk"), }, }, } @@ -632,15 +631,15 @@ func createAzureSnapshotWithCrossResourceGroupLinks(name, subscriptionID string) // 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"), + Name: new(name), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcompute.SnapshotProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), CreationData: &armcompute.CreationData{ - CreateOption: to.Ptr(armcompute.DiskCreateOptionEmpty), + CreateOption: new(armcompute.DiskCreateOptionEmpty), }, }, } diff --git a/sources/azure/manual/compute-virtual-machine-extension_test.go b/sources/azure/manual/compute-virtual-machine-extension_test.go index e64c4a17..8cbff84e 100644 --- a/sources/azure/manual/compute-virtual-machine-extension_test.go +++ b/sources/azure/manual/compute-virtual-machine-extension_test.go @@ -5,7 +5,6 @@ import ( "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" @@ -134,9 +133,9 @@ func TestComputeVirtualMachineExtension(t *testing.T) { extension := createAzureVirtualMachineExtension(extensionName, vmName) extension.Properties.ProtectedSettingsFromKeyVault = &armcompute.KeyVaultSecretReference{ SourceVault: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/different-rg/providers/Microsoft.KeyVault/vaults/test-keyvault"), + ID: new("/subscriptions/test-subscription/resourceGroups/different-rg/providers/Microsoft.KeyVault/vaults/test-keyvault"), }, - SecretURL: to.Ptr("https://test-keyvault.vault.azure.net/secrets/test-secret/version"), + SecretURL: new("https://test-keyvault.vault.azure.net/secrets/test-secret/version"), } mockClient := mocks.NewMockVirtualMachineExtensionsClient(ctrl) @@ -673,18 +672,18 @@ func TestComputeVirtualMachineExtension(t *testing.T) { func createAzureVirtualMachineExtension(extensionName, vmName string) *armcompute.VirtualMachineExtension { return &armcompute.VirtualMachineExtension{ - Name: to.Ptr(extensionName), - Location: to.Ptr("eastus"), - Type: to.Ptr("Microsoft.Compute/virtualMachines/extensions"), + Name: new(extensionName), + Location: new("eastus"), + Type: new("Microsoft.Compute/virtualMachines/extensions"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcompute.VirtualMachineExtensionProperties{ - Publisher: to.Ptr("Microsoft.Compute"), - Type: to.Ptr("CustomScriptExtension"), - TypeHandlerVersion: to.Ptr("1.10"), - ProvisioningState: to.Ptr("Succeeded"), + Publisher: new("Microsoft.Compute"), + Type: new("CustomScriptExtension"), + TypeHandlerVersion: new("1.10"), + ProvisioningState: new("Succeeded"), }, } } @@ -693,7 +692,7 @@ func createAzureVirtualMachineExtensionWithKeyVault(extensionName, vmName string extension := createAzureVirtualMachineExtension(extensionName, vmName) extension.Properties.ProtectedSettingsFromKeyVault = &armcompute.KeyVaultSecretReference{ SourceVault: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-keyvault"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-keyvault"), }, } return extension @@ -701,8 +700,8 @@ func createAzureVirtualMachineExtensionWithKeyVault(extensionName, vmName string func createAzureVirtualMachineExtensionWithSettingsURL(extensionName, vmName string) *armcompute.VirtualMachineExtension { extension := createAzureVirtualMachineExtension(extensionName, vmName) - extension.Properties.Settings = map[string]interface{}{ - "fileUris": []interface{}{ + extension.Properties.Settings = map[string]any{ + "fileUris": []any{ "https://example.com/scripts/script.sh", }, "commandToExecute": "bash script.sh", @@ -712,7 +711,7 @@ func createAzureVirtualMachineExtensionWithSettingsURL(extensionName, vmName str func createAzureVirtualMachineExtensionWithSettingsIP(extensionName, vmName string) *armcompute.VirtualMachineExtension { extension := createAzureVirtualMachineExtension(extensionName, vmName) - extension.Properties.Settings = map[string]interface{}{ + extension.Properties.Settings = map[string]any{ "serverIP": "10.0.0.1", "port": 8080, } @@ -721,7 +720,7 @@ func createAzureVirtualMachineExtensionWithSettingsIP(extensionName, vmName stri func createAzureVirtualMachineExtensionWithProtectedSettings(extensionName, vmName string) *armcompute.VirtualMachineExtension { extension := createAzureVirtualMachineExtension(extensionName, vmName) - extension.Properties.ProtectedSettings = map[string]interface{}{ + extension.Properties.ProtectedSettings = map[string]any{ "storageAccountName": "mystorageaccount", "storageAccountKey": "secret-key", "endpoint": "https://api.example.com/v1", @@ -733,16 +732,16 @@ func createAzureVirtualMachineExtensionWithAllLinks(extensionName, vmName string extension := createAzureVirtualMachineExtension(extensionName, vmName) extension.Properties.ProtectedSettingsFromKeyVault = &armcompute.KeyVaultSecretReference{ SourceVault: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-keyvault"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-keyvault"), }, } - extension.Properties.Settings = map[string]interface{}{ - "fileUris": []interface{}{ + extension.Properties.Settings = map[string]any{ + "fileUris": []any{ "https://example.com/scripts/script.sh", }, "serverIP": "10.0.0.1", } - extension.Properties.ProtectedSettings = map[string]interface{}{ + extension.Properties.ProtectedSettings = map[string]any{ "endpoint": "https://api.example.com/v1", } return extension 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 fa0ddc92..33b68cee 100644 --- a/sources/azure/manual/compute-virtual-machine-run-command_test.go +++ b/sources/azure/manual/compute-virtual-machine-run-command_test.go @@ -5,7 +5,6 @@ import ( "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" @@ -63,39 +62,39 @@ func (t *testVirtualMachineRunCommandsClient) NewListByVirtualMachinePager(resou func createAzureVirtualMachineRunCommand(runCommandName, vmName string) *armcompute.VirtualMachineRunCommand { return &armcompute.VirtualMachineRunCommand{ - Name: to.Ptr(runCommandName), - Location: to.Ptr("eastus"), + Name: new(runCommandName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcompute.VirtualMachineRunCommandProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), }, } } func createAzureVirtualMachineRunCommandWithBlobURIs(runCommandName, vmName string) *armcompute.VirtualMachineRunCommand { runCommand := createAzureVirtualMachineRunCommand(runCommandName, vmName) - runCommand.Properties.OutputBlobURI = to.Ptr("https://mystorageaccount.blob.core.windows.net/outputcontainer/output.log") - runCommand.Properties.ErrorBlobURI = to.Ptr("https://mystorageaccount.blob.core.windows.net/errorcontainer/error.log") + runCommand.Properties.OutputBlobURI = new("https://mystorageaccount.blob.core.windows.net/outputcontainer/output.log") + runCommand.Properties.ErrorBlobURI = new("https://mystorageaccount.blob.core.windows.net/errorcontainer/error.log") return runCommand } func createAzureVirtualMachineRunCommandWithHTTPScriptURI(runCommandName, vmName string) *armcompute.VirtualMachineRunCommand { runCommand := createAzureVirtualMachineRunCommand(runCommandName, vmName) runCommand.Properties.Source = &armcompute.VirtualMachineRunCommandScriptSource{ - ScriptURI: to.Ptr("https://example.com/scripts/script.sh"), + ScriptURI: new("https://example.com/scripts/script.sh"), } return runCommand } func createAzureVirtualMachineRunCommandWithAllLinks(runCommandName, vmName string) *armcompute.VirtualMachineRunCommand { runCommand := createAzureVirtualMachineRunCommand(runCommandName, vmName) - runCommand.Properties.OutputBlobURI = to.Ptr("https://mystorageaccount.blob.core.windows.net/outputcontainer/output.log") - runCommand.Properties.ErrorBlobURI = to.Ptr("https://mystorageaccount.blob.core.windows.net/errorcontainer/error.log") + runCommand.Properties.OutputBlobURI = new("https://mystorageaccount.blob.core.windows.net/outputcontainer/output.log") + runCommand.Properties.ErrorBlobURI = new("https://mystorageaccount.blob.core.windows.net/errorcontainer/error.log") runCommand.Properties.Source = &armcompute.VirtualMachineRunCommandScriptSource{ - ScriptURI: to.Ptr("https://mystorageaccount.blob.core.windows.net/scripts/script.sh"), + ScriptURI: new("https://mystorageaccount.blob.core.windows.net/scripts/script.sh"), } return runCommand } @@ -502,9 +501,9 @@ func TestComputeVirtualMachineRunCommand(t *testing.T) { t.Run("SkipItemsWithoutName", func(t *testing.T) { runCommandWithName := createAzureVirtualMachineRunCommand("run-command-1", vmName) runCommandWithoutName := &armcompute.VirtualMachineRunCommand{ - Location: to.Ptr("eastus"), + Location: new("eastus"), Properties: &armcompute.VirtualMachineRunCommandProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), }, } 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 62729dfa..22487b1d 100644 --- a/sources/azure/manual/compute-virtual-machine-scale-set_test.go +++ b/sources/azure/manual/compute-virtual-machine-scale-set_test.go @@ -7,7 +7,6 @@ import ( "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" @@ -504,7 +503,7 @@ func (m *MockVirtualMachineScaleSetsPager) More() bool { func (mr *MockVirtualMachineScaleSetsPagerMockRecorder) More() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeOf((*MockVirtualMachineScaleSetsPager)(nil).More)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeFor[func() bool]()) } func (m *MockVirtualMachineScaleSetsPager) NextPage(ctx context.Context) (armcompute.VirtualMachineScaleSetsClientListResponse, error) { @@ -515,34 +514,34 @@ func (m *MockVirtualMachineScaleSetsPager) NextPage(ctx context.Context) (armcom return ret0, ret1 } -func (mr *MockVirtualMachineScaleSetsPagerMockRecorder) NextPage(ctx interface{}) *gomock.Call { +func (mr *MockVirtualMachineScaleSetsPagerMockRecorder) NextPage(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeOf((*MockVirtualMachineScaleSetsPager)(nil).NextPage), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeFor[func(ctx context.Context) (armcompute.VirtualMachineScaleSetsClientListResponse, error)](), ctx) } // createAzureVirtualMachineScaleSet creates a mock Azure Virtual Machine Scale Set for testing func createAzureVirtualMachineScaleSet(scaleSetName, provisioningState string) *armcompute.VirtualMachineScaleSet { return &armcompute.VirtualMachineScaleSet{ - Name: to.Ptr(scaleSetName), - Location: to.Ptr("eastus"), + Name: new(scaleSetName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcompute.VirtualMachineScaleSetProperties{ - ProvisioningState: to.Ptr(provisioningState), + ProvisioningState: new(provisioningState), VirtualMachineProfile: &armcompute.VirtualMachineScaleSetVMProfile{ ExtensionProfile: &armcompute.VirtualMachineScaleSetExtensionProfile{ Extensions: []*armcompute.VirtualMachineScaleSetExtension{ { - Name: to.Ptr("CustomScriptExtension"), + Name: new("CustomScriptExtension"), Properties: &armcompute.VirtualMachineScaleSetExtensionProperties{ - Type: to.Ptr("CustomScriptExtension"), - Publisher: to.Ptr("Microsoft.Compute"), - TypeHandlerVersion: to.Ptr("1.10"), + Type: new("CustomScriptExtension"), + Publisher: new("Microsoft.Compute"), + TypeHandlerVersion: new("1.10"), ProtectedSettingsFromKeyVault: &armcompute.KeyVaultSecretReference{ SourceVault: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-keyvault-ext"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-keyvault-ext"), }, }, }, @@ -551,48 +550,48 @@ func createAzureVirtualMachineScaleSet(scaleSetName, provisioningState string) * }, NetworkProfile: &armcompute.VirtualMachineScaleSetNetworkProfile{ HealthProbe: &armcompute.APIEntityReference{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/loadBalancers/test-lb/probes/test-probe"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/loadBalancers/test-lb/probes/test-probe"), }, NetworkInterfaceConfigurations: []*armcompute.VirtualMachineScaleSetNetworkConfiguration{ { - Name: to.Ptr("nic-config"), + Name: new("nic-config"), Properties: &armcompute.VirtualMachineScaleSetNetworkConfigurationProperties{ NetworkSecurityGroup: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-nsg"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/test-nsg"), }, IPConfigurations: []*armcompute.VirtualMachineScaleSetIPConfiguration{ { - Name: to.Ptr("ip-config"), + Name: new("ip-config"), Properties: &armcompute.VirtualMachineScaleSetIPConfigurationProperties{ Subnet: &armcompute.APIEntityReference{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/default"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/default"), }, PublicIPAddressConfiguration: &armcompute.VirtualMachineScaleSetPublicIPAddressConfiguration{ - Name: to.Ptr("public-ip-config"), + Name: new("public-ip-config"), Properties: &armcompute.VirtualMachineScaleSetPublicIPAddressConfigurationProperties{ PublicIPPrefix: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/publicIPPrefixes/test-pip-prefix"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/publicIPPrefixes/test-pip-prefix"), }, }, }, LoadBalancerBackendAddressPools: []*armcompute.SubResource{ { - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/loadBalancers/test-lb/backendAddressPools/test-backend-pool"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/loadBalancers/test-lb/backendAddressPools/test-backend-pool"), }, }, LoadBalancerInboundNatPools: []*armcompute.SubResource{ { - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/loadBalancers/test-lb/inboundNatPools/test-nat-pool"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/loadBalancers/test-lb/inboundNatPools/test-nat-pool"), }, }, ApplicationGatewayBackendAddressPools: []*armcompute.SubResource{ { - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/applicationGateways/test-ag/backendAddressPools/test-ag-pool"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/applicationGateways/test-ag/backendAddressPools/test-ag-pool"), }, }, ApplicationSecurityGroups: []*armcompute.SubResource{ { - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/applicationSecurityGroups/test-asg"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/applicationSecurityGroups/test-asg"), }, }, }, @@ -604,22 +603,22 @@ func createAzureVirtualMachineScaleSet(scaleSetName, provisioningState string) * }, StorageProfile: &armcompute.VirtualMachineScaleSetStorageProfile{ ImageReference: &armcompute.ImageReference{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/images/test-image"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/images/test-image"), }, OSDisk: &armcompute.VirtualMachineScaleSetOSDisk{ - Name: to.Ptr("os-disk"), + Name: new("os-disk"), ManagedDisk: &armcompute.VirtualMachineScaleSetManagedDiskParameters{ DiskEncryptionSet: &armcompute.DiskEncryptionSetParameters{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/diskEncryptionSets/test-disk-encryption-set"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/diskEncryptionSets/test-disk-encryption-set"), }, }, }, DataDisks: []*armcompute.VirtualMachineScaleSetDataDisk{ { - Name: to.Ptr("data-disk-1"), + Name: new("data-disk-1"), ManagedDisk: &armcompute.VirtualMachineScaleSetManagedDiskParameters{ DiskEncryptionSet: &armcompute.DiskEncryptionSetParameters{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/diskEncryptionSets/test-disk-encryption-set-data"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/diskEncryptionSets/test-disk-encryption-set-data"), }, }, }, @@ -629,26 +628,26 @@ func createAzureVirtualMachineScaleSet(scaleSetName, provisioningState string) * Secrets: []*armcompute.VaultSecretGroup{ { SourceVault: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-keyvault"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.KeyVault/vaults/test-keyvault"), }, }, }, }, DiagnosticsProfile: &armcompute.DiagnosticsProfile{ BootDiagnostics: &armcompute.BootDiagnostics{ - StorageURI: to.Ptr("https://teststorageaccount.blob.core.windows.net/"), + StorageURI: new("https://teststorageaccount.blob.core.windows.net/"), }, }, }, ProximityPlacementGroup: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/proximityPlacementGroups/test-ppg"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/proximityPlacementGroups/test-ppg"), }, HostGroup: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/hostGroups/test-host-group"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/hostGroups/test-host-group"), }, }, Identity: &armcompute.VirtualMachineScaleSetIdentity{ - Type: to.Ptr(armcompute.ResourceIdentityTypeUserAssigned), + Type: new(armcompute.ResourceIdentityTypeUserAssigned), UserAssignedIdentities: map[string]*armcompute.UserAssignedIdentitiesValue{ "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity": {}, }, diff --git a/sources/azure/manual/compute-virtual-machine_test.go b/sources/azure/manual/compute-virtual-machine_test.go index a746365f..f565919d 100644 --- a/sources/azure/manual/compute-virtual-machine_test.go +++ b/sources/azure/manual/compute-virtual-machine_test.go @@ -6,7 +6,6 @@ import ( "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" @@ -314,26 +313,26 @@ func TestComputeVirtualMachine(t *testing.T) { // createAzureVirtualMachine creates a mock Azure VM for testing func createAzureVirtualMachine(vmName, provisioningState string) *armcompute.VirtualMachine { return &armcompute.VirtualMachine{ - Name: to.Ptr(vmName), - Location: to.Ptr("eastus"), + Name: new(vmName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcompute.VirtualMachineProperties{ - ProvisioningState: to.Ptr(provisioningState), + ProvisioningState: new(provisioningState), StorageProfile: &armcompute.StorageProfile{ OSDisk: &armcompute.OSDisk{ - Name: to.Ptr("os-disk"), + Name: new("os-disk"), ManagedDisk: &armcompute.ManagedDiskParameters{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/disks/os-disk"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/disks/os-disk"), }, }, DataDisks: []*armcompute.DataDisk{ { - Name: to.Ptr("data-disk-1"), + Name: new("data-disk-1"), ManagedDisk: &armcompute.ManagedDiskParameters{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/disks/data-disk-1"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/disks/data-disk-1"), }, }, }, @@ -341,19 +340,19 @@ func createAzureVirtualMachine(vmName, provisioningState string) *armcompute.Vir NetworkProfile: &armcompute.NetworkProfile{ NetworkInterfaces: []*armcompute.NetworkInterfaceReference{ { - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/networkInterfaces/test-nic"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/networkInterfaces/test-nic"), }, }, }, AvailabilitySet: &armcompute.SubResource{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/availabilitySets/test-avset"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Compute/availabilitySets/test-avset"), }, }, // Add VM extensions to Resources Resources: []*armcompute.VirtualMachineExtension{ { - Name: to.Ptr("CustomScriptExtension"), - Type: to.Ptr("Microsoft.Compute/virtualMachines/extensions"), + Name: new("CustomScriptExtension"), + Type: new("Microsoft.Compute/virtualMachines/extensions"), }, }, } diff --git a/sources/azure/manual/dbforpostgresql-database_test.go b/sources/azure/manual/dbforpostgresql-database_test.go index da9a097f..eba76125 100644 --- a/sources/azure/manual/dbforpostgresql-database_test.go +++ b/sources/azure/manual/dbforpostgresql-database_test.go @@ -5,7 +5,6 @@ import ( "errors" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" "go.uber.org/mock/gomock" @@ -193,8 +192,8 @@ func TestDBforPostgreSQLDatabase(t *testing.T) { database2 := &armpostgresqlflexibleservers.Database{ Name: nil, // Database with nil name should be skipped Properties: &armpostgresqlflexibleservers.DatabaseProperties{ - Charset: to.Ptr("UTF8"), - Collation: to.Ptr("en_US.utf8"), + Charset: new("UTF8"), + Collation: new("en_US.utf8"), }, } @@ -299,11 +298,11 @@ func createAzurePostgreSQLDatabase(serverName, databaseName string) *armpostgres databaseID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/" + serverName + "/databases/" + databaseName return &armpostgresqlflexibleservers.Database{ - Name: to.Ptr(databaseName), - ID: to.Ptr(databaseID), + Name: new(databaseName), + ID: new(databaseID), Properties: &armpostgresqlflexibleservers.DatabaseProperties{ - Charset: to.Ptr("UTF8"), - Collation: to.Ptr("en_US.utf8"), + Charset: new("UTF8"), + Collation: new("en_US.utf8"), }, } } diff --git a/sources/azure/manual/dbforpostgresql-flexible-server_test.go b/sources/azure/manual/dbforpostgresql-flexible-server_test.go index b6278f77..beb4448c 100644 --- a/sources/azure/manual/dbforpostgresql-flexible-server_test.go +++ b/sources/azure/manual/dbforpostgresql-flexible-server_test.go @@ -5,7 +5,6 @@ import ( "errors" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" "go.uber.org/mock/gomock" @@ -327,7 +326,7 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { server2 := &armpostgresqlflexibleservers.Server{ Name: nil, // Server with nil name should be skipped Properties: &armpostgresqlflexibleservers.ServerProperties{ - Version: to.Ptr(armpostgresqlflexibleservers.PostgresMajorVersion("14")), + Version: new(armpostgresqlflexibleservers.PostgresMajorVersion("14")), }, } @@ -421,10 +420,10 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { geoBackupIdentityID := "/subscriptions/sub-id/resourceGroups/rg-id/providers/Microsoft.ManagedIdentity/userAssignedIdentities/geo-identity" server.Properties.DataEncryption = &armpostgresqlflexibleservers.DataEncryption{ - PrimaryKeyURI: to.Ptr(primaryKeyURI), - PrimaryUserAssignedIdentityID: to.Ptr(primaryIdentityID), - GeoBackupKeyURI: to.Ptr(geoBackupKeyURI), - GeoBackupUserAssignedIdentityID: to.Ptr(geoBackupIdentityID), + PrimaryKeyURI: new(primaryKeyURI), + PrimaryUserAssignedIdentityID: new(primaryIdentityID), + GeoBackupKeyURI: new(geoBackupKeyURI), + GeoBackupUserAssignedIdentityID: new(geoBackupIdentityID), } mockClient := mocks.NewMockPostgreSQLFlexibleServersClient(ctrl) @@ -526,8 +525,8 @@ func TestDBforPostgreSQLFlexibleServer(t *testing.T) { replicaServerName := "replica-server" sourceServerID := "/subscriptions/sub-id/resourceGroups/source-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/source-server" server := createAzurePostgreSQLFlexibleServer(replicaServerName, "", "") - server.Properties.SourceServerResourceID = to.Ptr(sourceServerID) - server.Properties.ReplicationRole = to.Ptr(armpostgresqlflexibleservers.ReplicationRoleAsyncReplica) + server.Properties.SourceServerResourceID = new(sourceServerID) + server.Properties.ReplicationRole = new(armpostgresqlflexibleservers.ReplicationRoleAsyncReplica) mockClient := mocks.NewMockPostgreSQLFlexibleServersClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, replicaServerName, nil).Return( @@ -608,32 +607,32 @@ func createAzurePostgreSQLFlexibleServer(serverName, subnetID, fqdn string) *arm serverID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/" + serverName server := &armpostgresqlflexibleservers.Server{ - Name: to.Ptr(serverName), - ID: to.Ptr(serverID), - Location: to.Ptr("eastus"), + Name: new(serverName), + ID: new(serverID), + Location: new("eastus"), Properties: &armpostgresqlflexibleservers.ServerProperties{ - Version: to.Ptr(armpostgresqlflexibleservers.PostgresMajorVersion("14")), - State: to.Ptr(armpostgresqlflexibleservers.ServerStateReady), + Version: new(armpostgresqlflexibleservers.PostgresMajorVersion("14")), + State: new(armpostgresqlflexibleservers.ServerStateReady), }, SKU: &armpostgresqlflexibleservers.SKU{ - Name: to.Ptr("Standard_B1ms"), - Tier: to.Ptr(armpostgresqlflexibleservers.SKUTierBurstable), + Name: new("Standard_B1ms"), + Tier: new(armpostgresqlflexibleservers.SKUTierBurstable), }, Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } // Add network configuration if subnet ID is provided if subnetID != "" { server.Properties.Network = &armpostgresqlflexibleservers.Network{ - DelegatedSubnetResourceID: to.Ptr(subnetID), + DelegatedSubnetResourceID: new(subnetID), } } // Add FQDN if provided if fqdn != "" { - server.Properties.FullyQualifiedDomainName = to.Ptr(fqdn) + server.Properties.FullyQualifiedDomainName = new(fqdn) } return server diff --git a/sources/azure/manual/documentdb-database-accounts_test.go b/sources/azure/manual/documentdb-database-accounts_test.go index 4ebd8fb0..d59160b7 100644 --- a/sources/azure/manual/documentdb-database-accounts_test.go +++ b/sources/azure/manual/documentdb-database-accounts_test.go @@ -5,7 +5,6 @@ import ( "errors" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" "go.uber.org/mock/gomock" @@ -177,7 +176,7 @@ func TestDocumentDBDatabaseAccounts(t *testing.T) { account := &armcosmos.DatabaseAccountGetResults{ Name: nil, // No name field Properties: &armcosmos.DatabaseAccountGetProperties{ - ProvisioningState: to.Ptr("Succeeded"), + ProvisioningState: new("Succeeded"), }, } @@ -350,27 +349,27 @@ func TestDocumentDBDatabaseAccounts(t *testing.T) { // createAzureCosmosDBAccount creates a mock Azure Cosmos DB account with all linked resources func createAzureCosmosDBAccount(accountName, provisioningState, subscriptionID, resourceGroup string) *armcosmos.DatabaseAccountGetResults { return &armcosmos.DatabaseAccountGetResults{ - Name: to.Ptr(accountName), - Location: to.Ptr("eastus"), + Name: new(accountName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armcosmos.DatabaseAccountGetProperties{ - ProvisioningState: to.Ptr(provisioningState), + ProvisioningState: new(provisioningState), // Private Endpoint Connections PrivateEndpointConnections: []*armcosmos.PrivateEndpointConnection{ { Properties: &armcosmos.PrivateEndpointConnectionProperties{ PrivateEndpoint: &armcosmos.PrivateEndpointProperty{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-private-endpoint"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-private-endpoint"), }, }, }, { Properties: &armcosmos.PrivateEndpointConnectionProperties{ PrivateEndpoint: &armcosmos.PrivateEndpointProperty{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/privateEndpoints/test-private-endpoint-diff-rg"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/privateEndpoints/test-private-endpoint-diff-rg"), }, }, }, @@ -378,17 +377,17 @@ func createAzureCosmosDBAccount(accountName, provisioningState, subscriptionID, // Virtual Network Rules VirtualNetworkRules: []*armcosmos.VirtualNetworkRule{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), }, { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/virtualNetworks/test-vnet-diff-rg/subnets/test-subnet-diff-rg"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/virtualNetworks/test-vnet-diff-rg/subnets/test-subnet-diff-rg"), }, }, // Key Vault Key URI - KeyVaultKeyURI: to.Ptr("https://test-keyvault.vault.azure.net/keys/test-key/version"), + KeyVaultKeyURI: new("https://test-keyvault.vault.azure.net/keys/test-key/version"), }, Identity: &armcosmos.ManagedServiceIdentity{ - Type: to.Ptr(armcosmos.ResourceIdentityTypeUserAssigned), + Type: new(armcosmos.ResourceIdentityTypeUserAssigned), UserAssignedIdentities: map[string]*armcosmos.Components1Jq1T4ISchemasManagedserviceidentityPropertiesUserassignedidentitiesAdditionalproperties{ "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity": {}, "/subscriptions/" + subscriptionID + "/resourceGroups/identity-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity-diff-rg": {}, @@ -400,13 +399,13 @@ func createAzureCosmosDBAccount(accountName, provisioningState, subscriptionID, // createAzureCosmosDBAccountMinimal creates a minimal mock Azure Cosmos DB account without linked resources func createAzureCosmosDBAccountMinimal(accountName, provisioningState string) *armcosmos.DatabaseAccountGetResults { return &armcosmos.DatabaseAccountGetResults{ - Name: to.Ptr(accountName), - Location: to.Ptr("eastus"), + Name: new(accountName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcosmos.DatabaseAccountGetProperties{ - ProvisioningState: to.Ptr(provisioningState), + ProvisioningState: new(provisioningState), }, } } @@ -414,19 +413,19 @@ func createAzureCosmosDBAccountMinimal(accountName, provisioningState string) *a // createAzureCosmosDBAccountCrossRG creates a mock Azure Cosmos DB account with linked resources in different resource groups func createAzureCosmosDBAccountCrossRG(accountName, provisioningState, subscriptionID, resourceGroup string) *armcosmos.DatabaseAccountGetResults { return &armcosmos.DatabaseAccountGetResults{ - Name: to.Ptr(accountName), - Location: to.Ptr("eastus"), + Name: new(accountName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armcosmos.DatabaseAccountGetProperties{ - ProvisioningState: to.Ptr(provisioningState), + ProvisioningState: new(provisioningState), // Private Endpoint in different resource group PrivateEndpointConnections: []*armcosmos.PrivateEndpointConnection{ { Properties: &armcosmos.PrivateEndpointConnectionProperties{ PrivateEndpoint: &armcosmos.PrivateEndpointProperty{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/privateEndpoints/test-pe-diff-rg"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/privateEndpoints/test-pe-diff-rg"), }, }, }, @@ -434,12 +433,12 @@ func createAzureCosmosDBAccountCrossRG(accountName, provisioningState, subscript // Subnet in different resource group VirtualNetworkRules: []*armcosmos.VirtualNetworkRule{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), }, }, }, Identity: &armcosmos.ManagedServiceIdentity{ - Type: to.Ptr(armcosmos.ResourceIdentityTypeUserAssigned), + Type: new(armcosmos.ResourceIdentityTypeUserAssigned), UserAssignedIdentities: map[string]*armcosmos.Components1Jq1T4ISchemasManagedserviceidentityPropertiesUserassignedidentitiesAdditionalproperties{ "/subscriptions/" + subscriptionID + "/resourceGroups/identity-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity": {}, }, diff --git a/sources/azure/manual/keyvault-managed-hsm_test.go b/sources/azure/manual/keyvault-managed-hsm_test.go index af237d38..8a7fb39a 100644 --- a/sources/azure/manual/keyvault-managed-hsm_test.go +++ b/sources/azure/manual/keyvault-managed-hsm_test.go @@ -6,7 +6,6 @@ import ( "sync" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" @@ -203,7 +202,7 @@ func TestKeyVaultManagedHSM(t *testing.T) { hsm := &armkeyvault.ManagedHsm{ Name: nil, // No name field Properties: &armkeyvault.ManagedHsmProperties{ - TenantID: to.Ptr("test-tenant-id"), + TenantID: new("test-tenant-id"), }, } @@ -325,7 +324,7 @@ func TestKeyVaultManagedHSM(t *testing.T) { hsm2 := &armkeyvault.ManagedHsm{ Name: nil, // This should be skipped Properties: &armkeyvault.ManagedHsmProperties{ - TenantID: to.Ptr("test-tenant-id"), + TenantID: new("test-tenant-id"), }, } hsm3 := createAzureManagedHSM("test-managed-hsm-3", subscriptionID, resourceGroup) @@ -480,7 +479,7 @@ func TestKeyVaultManagedHSM(t *testing.T) { hsm2 := &armkeyvault.ManagedHsm{ Name: nil, // This should be skipped Properties: &armkeyvault.ManagedHsmProperties{ - TenantID: to.Ptr("test-tenant-id"), + TenantID: new("test-tenant-id"), }, } hsm3 := createAzureManagedHSM("test-managed-hsm-3", subscriptionID, resourceGroup) @@ -604,30 +603,30 @@ func TestKeyVaultManagedHSM(t *testing.T) { // createAzureManagedHSM creates a mock Azure Managed HSM with linked resources func createAzureManagedHSM(hsmName, subscriptionID, resourceGroup string) *armkeyvault.ManagedHsm { return &armkeyvault.ManagedHsm{ - Name: to.Ptr(hsmName), - Location: to.Ptr("eastus"), + Name: new(hsmName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armkeyvault.ManagedHsmProperties{ - TenantID: to.Ptr("test-tenant-id"), - HsmURI: to.Ptr("https://" + hsmName + ".managedhsm.azure.net"), + TenantID: new("test-tenant-id"), + HsmURI: new("https://" + hsmName + ".managedhsm.azure.net"), // Private Endpoint Connections (ID is the connection resource ID for child resource linking) PrivateEndpointConnections: []*armkeyvault.MHSMPrivateEndpointConnectionItem{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/managedHSMs/" + hsmName + "/privateEndpointConnections/test-pec-1"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/managedHSMs/" + hsmName + "/privateEndpointConnections/test-pec-1"), Properties: &armkeyvault.MHSMPrivateEndpointConnectionProperties{ PrivateEndpoint: &armkeyvault.MHSMPrivateEndpoint{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-private-endpoint"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-private-endpoint"), }, }, }, { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/managedHSMs/" + hsmName + "/privateEndpointConnections/test-pec-2"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.KeyVault/managedHSMs/" + hsmName + "/privateEndpointConnections/test-pec-2"), Properties: &armkeyvault.MHSMPrivateEndpointConnectionProperties{ PrivateEndpoint: &armkeyvault.MHSMPrivateEndpoint{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/privateEndpoints/test-private-endpoint-diff-rg"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/privateEndpoints/test-private-endpoint-diff-rg"), }, }, }, @@ -636,25 +635,25 @@ func createAzureManagedHSM(hsmName, subscriptionID, resourceGroup string) *armke NetworkACLs: &armkeyvault.MHSMNetworkRuleSet{ VirtualNetworkRules: []*armkeyvault.MHSMVirtualNetworkRule{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), }, { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/virtualNetworks/test-vnet-diff-rg/subnets/test-subnet-diff-rg"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/virtualNetworks/test-vnet-diff-rg/subnets/test-subnet-diff-rg"), }, }, IPRules: []*armkeyvault.MHSMIPRule{ { - Value: to.Ptr("192.168.1.1"), + Value: new("192.168.1.1"), }, { - Value: to.Ptr("10.0.0.0/24"), + Value: new("10.0.0.0/24"), }, }, }, }, // User Assigned Identities Identity: &armkeyvault.ManagedServiceIdentity{ - Type: to.Ptr(armkeyvault.ManagedServiceIdentityTypeUserAssigned), + Type: new(armkeyvault.ManagedServiceIdentityTypeUserAssigned), UserAssignedIdentities: map[string]*armkeyvault.UserAssignedIdentity{ "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity": {}, "/subscriptions/" + subscriptionID + "/resourceGroups/identity-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity-diff-rg": {}, @@ -666,13 +665,13 @@ func createAzureManagedHSM(hsmName, subscriptionID, resourceGroup string) *armke // createAzureManagedHSMMinimal creates a minimal mock Azure Managed HSM without linked resources func createAzureManagedHSMMinimal(hsmName string) *armkeyvault.ManagedHsm { return &armkeyvault.ManagedHsm{ - Name: to.Ptr(hsmName), - Location: to.Ptr("eastus"), + Name: new(hsmName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armkeyvault.ManagedHsmProperties{ - TenantID: to.Ptr("test-tenant-id"), + TenantID: new("test-tenant-id"), }, } } @@ -680,19 +679,19 @@ func createAzureManagedHSMMinimal(hsmName string) *armkeyvault.ManagedHsm { // createAzureManagedHSMCrossRG creates a mock Azure Managed HSM with linked resources in different resource groups func createAzureManagedHSMCrossRG(hsmName, subscriptionID, resourceGroup string) *armkeyvault.ManagedHsm { return &armkeyvault.ManagedHsm{ - Name: to.Ptr(hsmName), - Location: to.Ptr("eastus"), + Name: new(hsmName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armkeyvault.ManagedHsmProperties{ - TenantID: to.Ptr("test-tenant-id"), + TenantID: new("test-tenant-id"), // Private Endpoint in different resource group PrivateEndpointConnections: []*armkeyvault.MHSMPrivateEndpointConnectionItem{ { Properties: &armkeyvault.MHSMPrivateEndpointConnectionProperties{ PrivateEndpoint: &armkeyvault.MHSMPrivateEndpoint{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/privateEndpoints/test-pe-diff-rg"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/privateEndpoints/test-pe-diff-rg"), }, }, }, @@ -701,14 +700,14 @@ func createAzureManagedHSMCrossRG(hsmName, subscriptionID, resourceGroup string) NetworkACLs: &armkeyvault.MHSMNetworkRuleSet{ VirtualNetworkRules: []*armkeyvault.MHSMVirtualNetworkRule{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), }, }, }, }, // User Assigned Identity in different resource group Identity: &armkeyvault.ManagedServiceIdentity{ - Type: to.Ptr(armkeyvault.ManagedServiceIdentityTypeUserAssigned), + Type: new(armkeyvault.ManagedServiceIdentityTypeUserAssigned), UserAssignedIdentities: map[string]*armkeyvault.UserAssignedIdentity{ "/subscriptions/" + subscriptionID + "/resourceGroups/identity-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity-diff-rg": {}, }, diff --git a/sources/azure/manual/keyvault-secret_test.go b/sources/azure/manual/keyvault-secret_test.go index 0e8f23b1..c8814996 100644 --- a/sources/azure/manual/keyvault-secret_test.go +++ b/sources/azure/manual/keyvault-secret_test.go @@ -4,9 +4,9 @@ import ( "context" "errors" "fmt" + "slices" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" @@ -502,13 +502,7 @@ func TestKeyVaultSecret(t *testing.T) { } expectedPermission := "Microsoft.KeyVault/vaults/secrets/read" - found := false - for _, perm := range permissions { - if perm == expectedPermission { - found = true - break - } - } + found := slices.Contains(permissions, expectedPermission) if !found { t.Errorf("Expected IAMPermissions to include %s", expectedPermission) } @@ -578,16 +572,16 @@ func TestKeyVaultSecret(t *testing.T) { // createAzureSecret creates a mock Azure Key Vault secret with linked vault func createAzureSecret(secretName, subscriptionID, resourceGroup, vaultName string) *armkeyvault.Secret { return &armkeyvault.Secret{ - ID: to.Ptr(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.KeyVault/vaults/%s/secrets/%s", subscriptionID, resourceGroup, vaultName, secretName)), - Name: to.Ptr(secretName), - Type: to.Ptr("Microsoft.KeyVault/vaults/secrets"), + ID: new(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.KeyVault/vaults/%s/secrets/%s", subscriptionID, resourceGroup, vaultName, secretName)), + Name: new(secretName), + Type: new("Microsoft.KeyVault/vaults/secrets"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armkeyvault.SecretProperties{ - Value: to.Ptr("secret-value"), - SecretURI: to.Ptr(fmt.Sprintf("https://%s.vault.azure.net/secrets/%s", vaultName, secretName)), + Value: new("secret-value"), + SecretURI: new(fmt.Sprintf("https://%s.vault.azure.net/secrets/%s", vaultName, secretName)), }, } } @@ -595,13 +589,13 @@ func createAzureSecret(secretName, subscriptionID, resourceGroup, vaultName stri // createAzureSecretMinimal creates a minimal mock Azure Key Vault secret without ID (no linked resources) func createAzureSecretMinimal(secretName string) *armkeyvault.Secret { return &armkeyvault.Secret{ - Name: to.Ptr(secretName), - Type: to.Ptr("Microsoft.KeyVault/vaults/secrets"), + Name: new(secretName), + Type: new("Microsoft.KeyVault/vaults/secrets"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armkeyvault.SecretProperties{ - Value: to.Ptr("secret-value"), + Value: new("secret-value"), }, } } @@ -609,14 +603,14 @@ func createAzureSecretMinimal(secretName string) *armkeyvault.Secret { // createAzureSecretCrossRG creates a mock Azure Key Vault secret with vault in a different resource group func createAzureSecretCrossRG(secretName, subscriptionID, vaultResourceGroup, vaultName string) *armkeyvault.Secret { return &armkeyvault.Secret{ - ID: to.Ptr(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.KeyVault/vaults/%s/secrets/%s", subscriptionID, vaultResourceGroup, vaultName, secretName)), - Name: to.Ptr(secretName), - Type: to.Ptr("Microsoft.KeyVault/vaults/secrets"), + ID: new(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.KeyVault/vaults/%s/secrets/%s", subscriptionID, vaultResourceGroup, vaultName, secretName)), + Name: new(secretName), + Type: new("Microsoft.KeyVault/vaults/secrets"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armkeyvault.SecretProperties{ - Value: to.Ptr("secret-value"), + Value: new("secret-value"), }, } } diff --git a/sources/azure/manual/keyvault-vault_test.go b/sources/azure/manual/keyvault-vault_test.go index 267d6955..804adb1e 100644 --- a/sources/azure/manual/keyvault-vault_test.go +++ b/sources/azure/manual/keyvault-vault_test.go @@ -5,7 +5,6 @@ import ( "errors" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "go.uber.org/mock/gomock" @@ -184,7 +183,7 @@ func TestKeyVaultVault(t *testing.T) { vault := &armkeyvault.Vault{ Name: nil, // No name field Properties: &armkeyvault.VaultProperties{ - TenantID: to.Ptr("test-tenant-id"), + TenantID: new("test-tenant-id"), }, } @@ -379,27 +378,27 @@ func TestKeyVaultVault(t *testing.T) { // createAzureKeyVault creates a mock Azure Key Vault with linked resources func createAzureKeyVault(vaultName, subscriptionID, resourceGroup string) *armkeyvault.Vault { return &armkeyvault.Vault{ - Name: to.Ptr(vaultName), - Location: to.Ptr("eastus"), + Name: new(vaultName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armkeyvault.VaultProperties{ - TenantID: to.Ptr("test-tenant-id"), + TenantID: new("test-tenant-id"), // Private Endpoint Connections PrivateEndpointConnections: []*armkeyvault.PrivateEndpointConnectionItem{ { Properties: &armkeyvault.PrivateEndpointConnectionProperties{ PrivateEndpoint: &armkeyvault.PrivateEndpoint{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-private-endpoint"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-private-endpoint"), }, }, }, { Properties: &armkeyvault.PrivateEndpointConnectionProperties{ PrivateEndpoint: &armkeyvault.PrivateEndpoint{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/privateEndpoints/test-private-endpoint-diff-rg"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/privateEndpoints/test-private-endpoint-diff-rg"), }, }, }, @@ -408,21 +407,21 @@ func createAzureKeyVault(vaultName, subscriptionID, resourceGroup string) *armke NetworkACLs: &armkeyvault.NetworkRuleSet{ VirtualNetworkRules: []*armkeyvault.VirtualNetworkRule{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), }, { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/virtualNetworks/test-vnet-diff-rg/subnets/test-subnet-diff-rg"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/virtualNetworks/test-vnet-diff-rg/subnets/test-subnet-diff-rg"), }, }, IPRules: []*armkeyvault.IPRule{ - {Value: to.Ptr("192.168.1.100")}, - {Value: to.Ptr("10.0.0.0/24")}, + {Value: new("192.168.1.100")}, + {Value: new("10.0.0.0/24")}, }, }, // Vault URI for keys and secrets operations - VaultURI: to.Ptr("https://" + vaultName + ".vault.azure.net/"), + VaultURI: new("https://" + vaultName + ".vault.azure.net/"), // Managed HSM Pool Resource ID - HsmPoolResourceID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/hsm-rg/providers/Microsoft.KeyVault/managedHSMs/test-managed-hsm"), + HsmPoolResourceID: new("/subscriptions/" + subscriptionID + "/resourceGroups/hsm-rg/providers/Microsoft.KeyVault/managedHSMs/test-managed-hsm"), }, } } @@ -430,13 +429,13 @@ func createAzureKeyVault(vaultName, subscriptionID, resourceGroup string) *armke // createAzureKeyVaultMinimal creates a minimal mock Azure Key Vault without linked resources func createAzureKeyVaultMinimal(vaultName string) *armkeyvault.Vault { return &armkeyvault.Vault{ - Name: to.Ptr(vaultName), - Location: to.Ptr("eastus"), + Name: new(vaultName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armkeyvault.VaultProperties{ - TenantID: to.Ptr("test-tenant-id"), + TenantID: new("test-tenant-id"), }, } } @@ -444,19 +443,19 @@ func createAzureKeyVaultMinimal(vaultName string) *armkeyvault.Vault { // createAzureKeyVaultCrossRG creates a mock Azure Key Vault with linked resources in different resource groups func createAzureKeyVaultCrossRG(vaultName, subscriptionID, resourceGroup string) *armkeyvault.Vault { return &armkeyvault.Vault{ - Name: to.Ptr(vaultName), - Location: to.Ptr("eastus"), + Name: new(vaultName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armkeyvault.VaultProperties{ - TenantID: to.Ptr("test-tenant-id"), + TenantID: new("test-tenant-id"), // Private Endpoint in different resource group PrivateEndpointConnections: []*armkeyvault.PrivateEndpointConnectionItem{ { Properties: &armkeyvault.PrivateEndpointConnectionProperties{ PrivateEndpoint: &armkeyvault.PrivateEndpoint{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/privateEndpoints/test-pe-diff-rg"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/privateEndpoints/test-pe-diff-rg"), }, }, }, @@ -465,12 +464,12 @@ func createAzureKeyVaultCrossRG(vaultName, subscriptionID, resourceGroup string) NetworkACLs: &armkeyvault.NetworkRuleSet{ VirtualNetworkRules: []*armkeyvault.VirtualNetworkRule{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/different-rg/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), }, }, }, // Managed HSM in different resource group - HsmPoolResourceID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/hsm-rg/providers/Microsoft.KeyVault/managedHSMs/test-managed-hsm"), + HsmPoolResourceID: new("/subscriptions/" + subscriptionID + "/resourceGroups/hsm-rg/providers/Microsoft.KeyVault/managedHSMs/test-managed-hsm"), }, } } diff --git a/sources/azure/manual/links_helpers.go b/sources/azure/manual/links_helpers.go index eb7549c1..fc11304e 100644 --- a/sources/azure/manual/links_helpers.go +++ b/sources/azure/manual/links_helpers.go @@ -2,6 +2,7 @@ package manual import ( "net" + "slices" "strings" "github.com/overmindtech/cli/go/sdp-go" @@ -21,10 +22,8 @@ func appendLinkIfValid( if value == "" { return } - for _, skip := range skipValues { - if value == skip { - return - } + if slices.Contains(skipValues, value) { + return } if q := createQuery(value); q != nil { *queries = append(*queries, q) diff --git a/sources/azure/manual/managedidentity-user-assigned-identity_test.go b/sources/azure/manual/managedidentity-user-assigned-identity_test.go index 219dd86f..c4e6ff92 100644 --- a/sources/azure/manual/managedidentity-user-assigned-identity_test.go +++ b/sources/azure/manual/managedidentity-user-assigned-identity_test.go @@ -6,7 +6,6 @@ import ( "sync" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" "go.uber.org/mock/gomock" @@ -136,14 +135,14 @@ func TestManagedIdentityUserAssignedIdentity(t *testing.T) { identity1 := createAzureUserAssignedIdentity("test-identity-1") identity2 := &armmsi.Identity{ Name: nil, // Identity with nil name should be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armmsi.UserAssignedIdentityProperties{ - ClientID: to.Ptr("test-client-id-2"), - PrincipalID: to.Ptr("test-principal-id-2"), - TenantID: to.Ptr("test-tenant-id"), + ClientID: new("test-client-id-2"), + PrincipalID: new("test-principal-id-2"), + TenantID: new("test-tenant-id"), }, } @@ -299,16 +298,16 @@ func TestManagedIdentityUserAssignedIdentity(t *testing.T) { // createAzureUserAssignedIdentity creates a mock Azure User Assigned Identity for testing func createAzureUserAssignedIdentity(identityName string) *armmsi.Identity { return &armmsi.Identity{ - Name: to.Ptr(identityName), - Location: to.Ptr("eastus"), + Name: new(identityName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armmsi.UserAssignedIdentityProperties{ - ClientID: to.Ptr("test-client-id"), - PrincipalID: to.Ptr("test-principal-id"), - TenantID: to.Ptr("test-tenant-id"), + ClientID: new("test-client-id"), + PrincipalID: new("test-principal-id"), + TenantID: new("test-tenant-id"), }, } } diff --git a/sources/azure/manual/network-application-gateway_test.go b/sources/azure/manual/network-application-gateway_test.go index d69e14ea..9bb62f88 100644 --- a/sources/azure/manual/network-application-gateway_test.go +++ b/sources/azure/manual/network-application-gateway_test.go @@ -7,7 +7,6 @@ import ( "reflect" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" @@ -245,9 +244,9 @@ func TestNetworkApplicationGateway(t *testing.T) { t.Run("Get_WithNilName", func(t *testing.T) { applicationGateway := &armnetwork.ApplicationGateway{ Name: nil, // Application Gateway with nil name should cause an error - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } @@ -337,9 +336,9 @@ func TestNetworkApplicationGateway(t *testing.T) { ag1 := createAzureApplicationGateway("test-ag-1", subscriptionID, resourceGroup) ag2 := &armnetwork.ApplicationGateway{ Name: nil, // Application Gateway with nil name should be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } @@ -505,7 +504,7 @@ func TestNetworkApplicationGateway(t *testing.T) { } // Verify PredefinedRole - if roleInterface, ok := interface{}(wrapper).(interface{ PredefinedRole() string }); ok { + if roleInterface, ok := any(wrapper).(interface{ PredefinedRole() string }); ok { role := roleInterface.PredefinedRole() if role != "Reader" { t.Errorf("Expected PredefinedRole to be 'Reader', got %s", role) @@ -545,7 +544,7 @@ func (m *MockApplicationGatewaysPager) More() bool { func (mr *MockApplicationGatewaysPagerMockRecorder) More() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeOf((*MockApplicationGatewaysPager)(nil).More)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeFor[func() bool]()) } func (m *MockApplicationGatewaysPager) NextPage(ctx context.Context) (armnetwork.ApplicationGatewaysClientListResponse, error) { @@ -556,28 +555,28 @@ func (m *MockApplicationGatewaysPager) NextPage(ctx context.Context) (armnetwork return ret0, ret1 } -func (mr *MockApplicationGatewaysPagerMockRecorder) NextPage(ctx interface{}) *gomock.Call { +func (mr *MockApplicationGatewaysPagerMockRecorder) NextPage(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeOf((*MockApplicationGatewaysPager)(nil).NextPage), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeFor[func(ctx context.Context) (armnetwork.ApplicationGatewaysClientListResponse, error)](), ctx) } // createAzureApplicationGateway creates a mock Azure Application Gateway for testing func createAzureApplicationGateway(agName, subscriptionID, resourceGroup string) *armnetwork.ApplicationGateway { return &armnetwork.ApplicationGateway{ - Name: to.Ptr(agName), - Location: to.Ptr("eastus"), + Name: new(agName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armnetwork.ApplicationGatewayPropertiesFormat{ // GatewayIPConfigurations (Child Resource) GatewayIPConfigurations: []*armnetwork.ApplicationGatewayIPConfiguration{ { - Name: to.Ptr("gateway-ip-config"), + Name: new("gateway-ip-config"), Properties: &armnetwork.ApplicationGatewayIPConfigurationPropertiesFormat{ Subnet: &armnetwork.SubResource{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), }, }, }, @@ -585,23 +584,23 @@ func createAzureApplicationGateway(agName, subscriptionID, resourceGroup string) // FrontendIPConfigurations (Child Resource) FrontendIPConfigurations: []*armnetwork.ApplicationGatewayFrontendIPConfiguration{ { - Name: to.Ptr("frontend-ip-config"), + Name: new("frontend-ip-config"), Properties: &armnetwork.ApplicationGatewayFrontendIPConfigurationPropertiesFormat{ PublicIPAddress: &armnetwork.SubResource{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/publicIPAddresses/test-public-ip"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/publicIPAddresses/test-public-ip"), }, - PrivateIPAddress: to.Ptr("10.2.0.5"), + PrivateIPAddress: new("10.2.0.5"), }, }, }, // BackendAddressPools (Child Resource) BackendAddressPools: []*armnetwork.ApplicationGatewayBackendAddressPool{ { - Name: to.Ptr("backend-pool"), + Name: new("backend-pool"), Properties: &armnetwork.ApplicationGatewayBackendAddressPoolPropertiesFormat{ BackendAddresses: []*armnetwork.ApplicationGatewayBackendAddress{ { - IPAddress: to.Ptr("10.0.1.4"), + IPAddress: new("10.0.1.4"), }, }, }, @@ -610,76 +609,76 @@ func createAzureApplicationGateway(agName, subscriptionID, resourceGroup string) // HTTPListeners (Child Resource) HTTPListeners: []*armnetwork.ApplicationGatewayHTTPListener{ { - Name: to.Ptr("http-listener"), + Name: new("http-listener"), }, }, // BackendHTTPSettingsCollection (Child Resource) BackendHTTPSettingsCollection: []*armnetwork.ApplicationGatewayBackendHTTPSettings{ { - Name: to.Ptr("backend-http-settings"), + Name: new("backend-http-settings"), }, }, // RequestRoutingRules (Child Resource) RequestRoutingRules: []*armnetwork.ApplicationGatewayRequestRoutingRule{ { - Name: to.Ptr("routing-rule"), + Name: new("routing-rule"), }, }, // Probes (Child Resource) Probes: []*armnetwork.ApplicationGatewayProbe{ { - Name: to.Ptr("health-probe"), + Name: new("health-probe"), }, }, // SSLCertificates (Child Resource) SSLCertificates: []*armnetwork.ApplicationGatewaySSLCertificate{ { - Name: to.Ptr("ssl-cert"), + Name: new("ssl-cert"), Properties: &armnetwork.ApplicationGatewaySSLCertificatePropertiesFormat{ - KeyVaultSecretID: to.Ptr("https://test-keyvault.vault.azure.net/secrets/test-secret/version"), + KeyVaultSecretID: new("https://test-keyvault.vault.azure.net/secrets/test-secret/version"), }, }, }, // URLPathMaps (Child Resource) URLPathMaps: []*armnetwork.ApplicationGatewayURLPathMap{ { - Name: to.Ptr("url-path-map"), + Name: new("url-path-map"), }, }, // AuthenticationCertificates (Child Resource) AuthenticationCertificates: []*armnetwork.ApplicationGatewayAuthenticationCertificate{ { - Name: to.Ptr("auth-cert"), + Name: new("auth-cert"), }, }, // TrustedRootCertificates (Child Resource) TrustedRootCertificates: []*armnetwork.ApplicationGatewayTrustedRootCertificate{ { - Name: to.Ptr("trusted-root-cert"), + Name: new("trusted-root-cert"), Properties: &armnetwork.ApplicationGatewayTrustedRootCertificatePropertiesFormat{ - KeyVaultSecretID: to.Ptr("https://test-trusted-keyvault.vault.azure.net/secrets/test-trusted-secret/version"), + KeyVaultSecretID: new("https://test-trusted-keyvault.vault.azure.net/secrets/test-trusted-secret/version"), }, }, }, // RewriteRuleSets (Child Resource) RewriteRuleSets: []*armnetwork.ApplicationGatewayRewriteRuleSet{ { - Name: to.Ptr("rewrite-rule-set"), + Name: new("rewrite-rule-set"), }, }, // RedirectConfigurations (Child Resource) RedirectConfigurations: []*armnetwork.ApplicationGatewayRedirectConfiguration{ { - Name: to.Ptr("redirect-config"), + Name: new("redirect-config"), }, }, // FirewallPolicy (External Resource) FirewallPolicy: &armnetwork.SubResource{ - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/test-waf-policy"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies/test-waf-policy"), }, }, Identity: &armnetwork.ManagedServiceIdentity{ - Type: to.Ptr(armnetwork.ResourceIdentityTypeUserAssigned), + Type: new(armnetwork.ResourceIdentityTypeUserAssigned), UserAssignedIdentities: map[string]*armnetwork.Components1Jq1T4ISchemasManagedserviceidentityPropertiesUserassignedidentitiesAdditionalproperties{ "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity": {}, }, @@ -693,10 +692,10 @@ func createAzureApplicationGatewayWithDifferentScopePublicIP(agName, subscriptio // Override FrontendIPConfiguration with PublicIPAddress in different scope ag.Properties.FrontendIPConfigurations = []*armnetwork.ApplicationGatewayFrontendIPConfiguration{ { - Name: to.Ptr("frontend-ip-config"), + Name: new("frontend-ip-config"), Properties: &armnetwork.ApplicationGatewayFrontendIPConfigurationPropertiesFormat{ PublicIPAddress: &armnetwork.SubResource{ - ID: to.Ptr("/subscriptions/" + otherSubscriptionID + "/resourceGroups/" + otherResourceGroup + "/providers/Microsoft.Network/publicIPAddresses/test-public-ip"), + ID: new("/subscriptions/" + otherSubscriptionID + "/resourceGroups/" + otherResourceGroup + "/providers/Microsoft.Network/publicIPAddresses/test-public-ip"), }, }, }, diff --git a/sources/azure/manual/network-load-balancer_test.go b/sources/azure/manual/network-load-balancer_test.go index a7396166..6f028cf6 100644 --- a/sources/azure/manual/network-load-balancer_test.go +++ b/sources/azure/manual/network-load-balancer_test.go @@ -5,9 +5,9 @@ import ( "errors" "fmt" "reflect" + "slices" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" @@ -209,9 +209,9 @@ func TestNetworkLoadBalancer(t *testing.T) { lb1 := createAzureLoadBalancer("test-lb-1", subscriptionID, resourceGroup) lb2 := &armnetwork.LoadBalancer{ Name: nil, // Load balancer with nil name should be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armnetwork.LoadBalancerPropertiesFormat{}, } @@ -317,13 +317,7 @@ func TestNetworkLoadBalancer(t *testing.T) { t.Error("Expected IAMPermissions to return at least one permission") } expectedPermission := "Microsoft.Network/loadBalancers/read" - found := false - for _, perm := range permissions { - if perm == expectedPermission { - found = true - break - } - } + found := slices.Contains(permissions, expectedPermission) if !found { t.Errorf("Expected IAMPermissions to include %s", expectedPermission) } @@ -493,7 +487,7 @@ func (m *MockLoadBalancersPager) More() bool { func (mr *MockLoadBalancersPagerMockRecorder) More() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeOf((*MockLoadBalancersPager)(nil).More)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeFor[func() bool]()) } func (m *MockLoadBalancersPager) NextPage(ctx context.Context) (armnetwork.LoadBalancersClientListResponse, error) { @@ -504,9 +498,9 @@ func (m *MockLoadBalancersPager) NextPage(ctx context.Context) (armnetwork.LoadB return ret0, ret1 } -func (mr *MockLoadBalancersPagerMockRecorder) NextPage(ctx interface{}) *gomock.Call { +func (mr *MockLoadBalancersPagerMockRecorder) NextPage(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeOf((*MockLoadBalancersPager)(nil).NextPage), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeFor[func(ctx context.Context) (armnetwork.LoadBalancersClientListResponse, error)](), ctx) } // createAzureLoadBalancer creates a mock Azure load balancer for testing with all linked resources @@ -516,61 +510,61 @@ func createAzureLoadBalancer(lbName, subscriptionID, resourceGroup string) *armn nicID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/networkInterfaces/test-nic/ipConfigurations/ipconfig1", subscriptionID, resourceGroup) return &armnetwork.LoadBalancer{ - Name: to.Ptr(lbName), - Location: to.Ptr("eastus"), + Name: new(lbName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armnetwork.LoadBalancerPropertiesFormat{ FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{ { - Name: to.Ptr("frontend-ip-config"), + Name: new("frontend-ip-config"), Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{ PublicIPAddress: &armnetwork.PublicIPAddress{ - ID: to.Ptr(publicIPID), + ID: new(publicIPID), }, Subnet: &armnetwork.Subnet{ - ID: to.Ptr(subnetID), + ID: new(subnetID), }, // PrivateIPAddress is present when using a subnet (internal load balancer) - PrivateIPAddress: to.Ptr("10.2.0.5"), + PrivateIPAddress: new("10.2.0.5"), }, }, }, BackendAddressPools: []*armnetwork.BackendAddressPool{ { - Name: to.Ptr("backend-pool"), + Name: new("backend-pool"), }, }, InboundNatRules: []*armnetwork.InboundNatRule{ { - Name: to.Ptr("inbound-nat-rule"), + Name: new("inbound-nat-rule"), Properties: &armnetwork.InboundNatRulePropertiesFormat{ BackendIPConfiguration: &armnetwork.InterfaceIPConfiguration{ - ID: to.Ptr(nicID), + ID: new(nicID), }, }, }, }, LoadBalancingRules: []*armnetwork.LoadBalancingRule{ { - Name: to.Ptr("lb-rule"), + Name: new("lb-rule"), }, }, Probes: []*armnetwork.Probe{ { - Name: to.Ptr("probe"), + Name: new("probe"), }, }, OutboundRules: []*armnetwork.OutboundRule{ { - Name: to.Ptr("outbound-rule"), + Name: new("outbound-rule"), }, }, InboundNatPools: []*armnetwork.InboundNatPool{ { - Name: to.Ptr("nat-pool"), + Name: new("nat-pool"), }, }, }, @@ -582,18 +576,18 @@ func createAzureLoadBalancerWithDifferentScopePublicIP(lbName, subscriptionID, r publicIPID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/publicIPAddresses/test-public-ip", otherSub, otherRG) return &armnetwork.LoadBalancer{ - Name: to.Ptr(lbName), - Location: to.Ptr("eastus"), + Name: new(lbName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armnetwork.LoadBalancerPropertiesFormat{ FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{ { - Name: to.Ptr("frontend-ip-config"), + Name: new("frontend-ip-config"), Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{ PublicIPAddress: &armnetwork.PublicIPAddress{ - ID: to.Ptr(publicIPID), + ID: new(publicIPID), }, }, }, @@ -607,18 +601,18 @@ func createAzureLoadBalancerWithDifferentScopeSubnet(lbName, subscriptionID, res subnetID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet", otherSub, otherRG) return &armnetwork.LoadBalancer{ - Name: to.Ptr(lbName), - Location: to.Ptr("eastus"), + Name: new(lbName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armnetwork.LoadBalancerPropertiesFormat{ FrontendIPConfigurations: []*armnetwork.FrontendIPConfiguration{ { - Name: to.Ptr("frontend-ip-config"), + Name: new("frontend-ip-config"), Properties: &armnetwork.FrontendIPConfigurationPropertiesFormat{ Subnet: &armnetwork.Subnet{ - ID: to.Ptr(subnetID), + ID: new(subnetID), }, }, }, diff --git a/sources/azure/manual/network-network-interface_test.go b/sources/azure/manual/network-network-interface_test.go index dd23559a..413aa50b 100644 --- a/sources/azure/manual/network-network-interface_test.go +++ b/sources/azure/manual/network-network-interface_test.go @@ -4,9 +4,9 @@ import ( "context" "errors" "reflect" + "slices" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" @@ -222,16 +222,16 @@ func TestNetworkNetworkInterface(t *testing.T) { nic1 := createAzureNetworkInterface("test-nic-1", "test-vm-1", "test-nsg-1") nic2 := &armnetwork.Interface{ Name: nil, // NIC with nil name should cause an error in azureNetworkInterfaceToSDPItem - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armnetwork.InterfacePropertiesFormat{ IPConfigurations: []*armnetwork.InterfaceIPConfiguration{ { - Name: to.Ptr("ipconfig1"), + Name: new("ipconfig1"), Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{ - PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic), + PrivateIPAllocationMethod: new(armnetwork.IPAllocationMethodDynamic), }, }, }, @@ -332,13 +332,7 @@ func TestNetworkNetworkInterface(t *testing.T) { t.Error("Expected IAMPermissions to return at least one permission") } expectedPermission := "Microsoft.Network/networkInterfaces/read" - found := false - for _, perm := range permissions { - if perm == expectedPermission { - found = true - break - } - } + found := slices.Contains(permissions, expectedPermission) if !found { t.Errorf("Expected IAMPermissions to include %s", expectedPermission) } @@ -400,7 +394,7 @@ func (m *MockNetworkInterfacesPager) More() bool { func (mr *MockNetworkInterfacesPagerMockRecorder) More() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeOf((*MockNetworkInterfacesPager)(nil).More)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeFor[func() bool]()) } func (m *MockNetworkInterfacesPager) NextPage(ctx context.Context) (armnetwork.InterfacesClientListResponse, error) { @@ -411,9 +405,9 @@ func (m *MockNetworkInterfacesPager) NextPage(ctx context.Context) (armnetwork.I return ret0, ret1 } -func (mr *MockNetworkInterfacesPagerMockRecorder) NextPage(ctx interface{}) *gomock.Call { +func (mr *MockNetworkInterfacesPagerMockRecorder) NextPage(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeOf((*MockNetworkInterfacesPager)(nil).NextPage), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeFor[func(ctx context.Context) (armnetwork.InterfacesClientListResponse, error)](), ctx) } // createAzureNetworkInterface creates a mock Azure network interface for testing @@ -422,24 +416,24 @@ func createAzureNetworkInterface(nicName, vmName, nsgName string) *armnetwork.In nsgID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/" + nsgName return &armnetwork.Interface{ - Name: to.Ptr(nicName), - Location: to.Ptr("eastus"), + Name: new(nicName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armnetwork.InterfacePropertiesFormat{ VirtualMachine: &armnetwork.SubResource{ - ID: to.Ptr(vmID), + ID: new(vmID), }, NetworkSecurityGroup: &armnetwork.SecurityGroup{ - ID: to.Ptr(nsgID), + ID: new(nsgID), }, IPConfigurations: []*armnetwork.InterfaceIPConfiguration{ { - Name: to.Ptr("ipconfig1"), + Name: new("ipconfig1"), Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{ - PrivateIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodDynamic), + PrivateIPAllocationMethod: new(armnetwork.IPAllocationMethodDynamic), }, }, }, @@ -452,7 +446,7 @@ func createAzureNetworkInterfaceWithDNSServers(nicName, vmName, nsgName string, nic := createAzureNetworkInterface(nicName, vmName, nsgName) ptrs := make([]*string, len(dnsServers)) for i := range dnsServers { - ptrs[i] = to.Ptr(dnsServers[i]) + ptrs[i] = new(dnsServers[i]) } nic.Properties.DNSSettings = &armnetwork.InterfaceDNSSettings{ DNSServers: ptrs, diff --git a/sources/azure/manual/network-network-security-group_test.go b/sources/azure/manual/network-network-security-group_test.go index 72f37f56..4080c289 100644 --- a/sources/azure/manual/network-network-security-group_test.go +++ b/sources/azure/manual/network-network-security-group_test.go @@ -4,9 +4,9 @@ import ( "context" "errors" "reflect" + "slices" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" @@ -133,9 +133,9 @@ func TestNetworkNetworkSecurityGroup(t *testing.T) { t.Run("Get_WithNilName", func(t *testing.T) { nsg := &armnetwork.SecurityGroup{ Name: nil, // NSG with nil name should cause an error - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } @@ -212,9 +212,9 @@ func TestNetworkNetworkSecurityGroup(t *testing.T) { nsg1 := createAzureNetworkSecurityGroup("test-nsg-1") nsg2 := &armnetwork.SecurityGroup{ Name: nil, // NSG with nil name should be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } @@ -306,20 +306,20 @@ func TestNetworkNetworkSecurityGroup(t *testing.T) { otherSubscriptionID := "other-subscription" nsg := &armnetwork.SecurityGroup{ - Name: to.Ptr(nsgName), - Location: to.Ptr("eastus"), + Name: new(nsgName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armnetwork.SecurityGroupPropertiesFormat{ Subnets: []*armnetwork.Subnet{ { - ID: to.Ptr("/subscriptions/" + otherSubscriptionID + "/resourceGroups/" + otherResourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), + ID: new("/subscriptions/" + otherSubscriptionID + "/resourceGroups/" + otherResourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), }, }, NetworkInterfaces: []*armnetwork.Interface{ { - ID: to.Ptr("/subscriptions/" + otherSubscriptionID + "/resourceGroups/" + otherResourceGroup + "/providers/Microsoft.Network/networkInterfaces/test-nic"), + ID: new("/subscriptions/" + otherSubscriptionID + "/resourceGroups/" + otherResourceGroup + "/providers/Microsoft.Network/networkInterfaces/test-nic"), }, }, }, @@ -386,13 +386,7 @@ func TestNetworkNetworkSecurityGroup(t *testing.T) { t.Error("Expected IAMPermissions to return at least one permission") } expectedPermission := "Microsoft.Network/networkSecurityGroups/read" - found := false - for _, perm := range permissions { - if perm == expectedPermission { - found = true - break - } - } + found := slices.Contains(permissions, expectedPermission) if !found { t.Errorf("Expected IAMPermissions to include %s", expectedPermission) } @@ -472,7 +466,7 @@ func (m *MockNetworkSecurityGroupsPager) More() bool { func (mr *MockNetworkSecurityGroupsPagerMockRecorder) More() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeOf((*MockNetworkSecurityGroupsPager)(nil).More)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeFor[func() bool]()) } func (m *MockNetworkSecurityGroupsPager) NextPage(ctx context.Context) (armnetwork.SecurityGroupsClientListResponse, error) { @@ -483,9 +477,9 @@ func (m *MockNetworkSecurityGroupsPager) NextPage(ctx context.Context) (armnetwo return ret0, ret1 } -func (mr *MockNetworkSecurityGroupsPagerMockRecorder) NextPage(ctx interface{}) *gomock.Call { +func (mr *MockNetworkSecurityGroupsPagerMockRecorder) NextPage(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeOf((*MockNetworkSecurityGroupsPager)(nil).NextPage), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeFor[func(ctx context.Context) (armnetwork.SecurityGroupsClientListResponse, error)](), ctx) } // createAzureNetworkSecurityGroup creates a mock Azure network security group for testing @@ -494,29 +488,29 @@ func createAzureNetworkSecurityGroup(nsgName string) *armnetwork.SecurityGroup { resourceGroup := "test-rg" return &armnetwork.SecurityGroup{ - Name: to.Ptr(nsgName), - Location: to.Ptr("eastus"), + Name: new(nsgName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armnetwork.SecurityGroupPropertiesFormat{ // SecurityRules (child resources) SecurityRules: []*armnetwork.SecurityRule{ { - Name: to.Ptr("test-security-rule"), + Name: new("test-security-rule"), Properties: &armnetwork.SecurityRulePropertiesFormat{ - Priority: to.Ptr(int32(1000)), - Direction: to.Ptr(armnetwork.SecurityRuleDirectionInbound), - Access: to.Ptr(armnetwork.SecurityRuleAccessAllow), + Priority: new(int32(1000)), + Direction: new(armnetwork.SecurityRuleDirectionInbound), + Access: new(armnetwork.SecurityRuleAccessAllow), SourceApplicationSecurityGroups: []*armnetwork.ApplicationSecurityGroup{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/applicationSecurityGroups/test-asg-source"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/applicationSecurityGroups/test-asg-source"), }, }, DestinationApplicationSecurityGroups: []*armnetwork.ApplicationSecurityGroup{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/applicationSecurityGroups/test-asg-dest"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/applicationSecurityGroups/test-asg-dest"), }, }, }, @@ -525,14 +519,14 @@ func createAzureNetworkSecurityGroup(nsgName string) *armnetwork.SecurityGroup { // DefaultSecurityRules (child resources) DefaultSecurityRules: []*armnetwork.SecurityRule{ { - Name: to.Ptr("AllowVnetInBound"), + Name: new("AllowVnetInBound"), Properties: &armnetwork.SecurityRulePropertiesFormat{ - Priority: to.Ptr(int32(65000)), - Direction: to.Ptr(armnetwork.SecurityRuleDirectionInbound), - Access: to.Ptr(armnetwork.SecurityRuleAccessAllow), + Priority: new(int32(65000)), + Direction: new(armnetwork.SecurityRuleDirectionInbound), + Access: new(armnetwork.SecurityRuleAccessAllow), SourceApplicationSecurityGroups: []*armnetwork.ApplicationSecurityGroup{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/applicationSecurityGroups/test-asg-default-source"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/applicationSecurityGroups/test-asg-default-source"), }, }, }, @@ -541,13 +535,13 @@ func createAzureNetworkSecurityGroup(nsgName string) *armnetwork.SecurityGroup { // Subnets (external resources) Subnets: []*armnetwork.Subnet{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), }, }, // NetworkInterfaces (external resources) NetworkInterfaces: []*armnetwork.Interface{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/networkInterfaces/test-nic"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/networkInterfaces/test-nic"), }, }, }, diff --git a/sources/azure/manual/network-public-ip-address_test.go b/sources/azure/manual/network-public-ip-address_test.go index 9fb6ee7b..fb7b0559 100644 --- a/sources/azure/manual/network-public-ip-address_test.go +++ b/sources/azure/manual/network-public-ip-address_test.go @@ -4,9 +4,9 @@ import ( "context" "errors" "reflect" + "slices" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" @@ -276,9 +276,9 @@ func TestNetworkPublicIPAddress(t *testing.T) { publicIP1 := createAzurePublicIPAddress("test-public-ip-1", "", "", "", "", "") publicIP2 := &armnetwork.PublicIPAddress{ Name: nil, // Public IP with nil name will be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armnetwork.PublicIPAddressPropertiesFormat{}, } @@ -384,13 +384,7 @@ func TestNetworkPublicIPAddress(t *testing.T) { t.Error("Expected IAMPermissions to return at least one permission") } expectedPermission := "Microsoft.Network/publicIPAddresses/read" - found := false - for _, perm := range permissions { - if perm == expectedPermission { - found = true - break - } - } + found := slices.Contains(permissions, expectedPermission) if !found { t.Errorf("Expected IAMPermissions to include %s", expectedPermission) } @@ -454,7 +448,7 @@ func (m *MockPublicIPAddressesPager) More() bool { func (mr *MockPublicIPAddressesPagerMockRecorder) More() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeOf((*MockPublicIPAddressesPager)(nil).More)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeFor[func() bool]()) } func (m *MockPublicIPAddressesPager) NextPage(ctx context.Context) (armnetwork.PublicIPAddressesClientListResponse, error) { @@ -465,24 +459,24 @@ func (m *MockPublicIPAddressesPager) NextPage(ctx context.Context) (armnetwork.P return ret0, ret1 } -func (mr *MockPublicIPAddressesPagerMockRecorder) NextPage(ctx interface{}) *gomock.Call { +func (mr *MockPublicIPAddressesPagerMockRecorder) NextPage(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeOf((*MockPublicIPAddressesPager)(nil).NextPage), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeFor[func(ctx context.Context) (armnetwork.PublicIPAddressesClientListResponse, error)](), ctx) } // createAzurePublicIPAddress creates a mock Azure public IP address for testing func createAzurePublicIPAddress(name, nicName, prefixName, natGatewayName, ddosPlanName, loadBalancerName string) *armnetwork.PublicIPAddress { publicIP := &armnetwork.PublicIPAddress{ - Name: to.Ptr(name), - Location: to.Ptr("eastus"), + Name: new(name), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armnetwork.PublicIPAddressPropertiesFormat{ - PublicIPAddressVersion: to.Ptr(armnetwork.IPVersionIPv4), - PublicIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodStatic), - IPAddress: to.Ptr("203.0.113.1"), // Add IP address for testing + PublicIPAddressVersion: new(armnetwork.IPVersionIPv4), + PublicIPAllocationMethod: new(armnetwork.IPAllocationMethodStatic), + IPAddress: new("203.0.113.1"), // Add IP address for testing }, } @@ -490,7 +484,7 @@ func createAzurePublicIPAddress(name, nicName, prefixName, natGatewayName, ddosP if nicName != "" { ipConfigID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/networkInterfaces/" + nicName + "/ipConfigurations/ipconfig1" publicIP.Properties.IPConfiguration = &armnetwork.IPConfiguration{ - ID: to.Ptr(ipConfigID), + ID: new(ipConfigID), } } @@ -498,7 +492,7 @@ func createAzurePublicIPAddress(name, nicName, prefixName, natGatewayName, ddosP if prefixName != "" { prefixID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/publicIPPrefixes/" + prefixName publicIP.Properties.PublicIPPrefix = &armnetwork.SubResource{ - ID: to.Ptr(prefixID), + ID: new(prefixID), } } @@ -506,7 +500,7 @@ func createAzurePublicIPAddress(name, nicName, prefixName, natGatewayName, ddosP if natGatewayName != "" { natGatewayID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/natGateways/" + natGatewayName publicIP.Properties.NatGateway = &armnetwork.NatGateway{ - ID: to.Ptr(natGatewayID), + ID: new(natGatewayID), } } @@ -515,7 +509,7 @@ func createAzurePublicIPAddress(name, nicName, prefixName, natGatewayName, ddosP ddosPlanID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/ddosProtectionPlans/" + ddosPlanName publicIP.Properties.DdosSettings = &armnetwork.DdosSettings{ DdosProtectionPlan: &armnetwork.SubResource{ - ID: to.Ptr(ddosPlanID), + ID: new(ddosPlanID), }, } } @@ -524,7 +518,7 @@ func createAzurePublicIPAddress(name, nicName, prefixName, natGatewayName, ddosP if loadBalancerName != "" { lbIPConfigID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/loadBalancers/" + loadBalancerName + "/frontendIPConfigurations/frontendIPConfig1" publicIP.Properties.IPConfiguration = &armnetwork.IPConfiguration{ - ID: to.Ptr(lbIPConfigID), + ID: new(lbIPConfigID), } } @@ -536,16 +530,16 @@ func createAzurePublicIPAddressWithLinkedIP(name, linkedIPName, subscriptionID, linkedIPID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/publicIPAddresses/" + linkedIPName return &armnetwork.PublicIPAddress{ - Name: to.Ptr(name), - Location: to.Ptr("eastus"), + Name: new(name), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armnetwork.PublicIPAddressPropertiesFormat{ - PublicIPAddressVersion: to.Ptr(armnetwork.IPVersionIPv4), - PublicIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodStatic), + PublicIPAddressVersion: new(armnetwork.IPVersionIPv4), + PublicIPAllocationMethod: new(armnetwork.IPAllocationMethodStatic), LinkedPublicIPAddress: &armnetwork.PublicIPAddress{ - ID: to.Ptr(linkedIPID), + ID: new(linkedIPID), }, }, } @@ -556,16 +550,16 @@ func createAzurePublicIPAddressWithServiceIP(name, serviceIPName, subscriptionID serviceIPID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/publicIPAddresses/" + serviceIPName return &armnetwork.PublicIPAddress{ - Name: to.Ptr(name), - Location: to.Ptr("eastus"), + Name: new(name), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armnetwork.PublicIPAddressPropertiesFormat{ - PublicIPAddressVersion: to.Ptr(armnetwork.IPVersionIPv4), - PublicIPAllocationMethod: to.Ptr(armnetwork.IPAllocationMethodStatic), + PublicIPAddressVersion: new(armnetwork.IPVersionIPv4), + PublicIPAllocationMethod: new(armnetwork.IPAllocationMethodStatic), ServicePublicIPAddress: &armnetwork.PublicIPAddress{ - ID: to.Ptr(serviceIPID), + ID: new(serviceIPID), }, }, } diff --git a/sources/azure/manual/network-route-table_test.go b/sources/azure/manual/network-route-table_test.go index abd5eca2..c7422ea4 100644 --- a/sources/azure/manual/network-route-table_test.go +++ b/sources/azure/manual/network-route-table_test.go @@ -4,9 +4,9 @@ import ( "context" "errors" "reflect" + "slices" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" @@ -109,9 +109,9 @@ func TestNetworkRouteTable(t *testing.T) { t.Run("Get_WithNilName", func(t *testing.T) { routeTable := &armnetwork.RouteTable{ Name: nil, // Route table with nil name should cause an error - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } @@ -188,9 +188,9 @@ func TestNetworkRouteTable(t *testing.T) { routeTable1 := createAzureRouteTable("test-route-table-1") routeTable2 := &armnetwork.RouteTable{ Name: nil, // Route table with nil name should be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, } @@ -282,15 +282,15 @@ func TestNetworkRouteTable(t *testing.T) { otherSubscriptionID := "other-subscription" routeTable := &armnetwork.RouteTable{ - Name: to.Ptr(routeTableName), - Location: to.Ptr("eastus"), + Name: new(routeTableName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armnetwork.RouteTablePropertiesFormat{ Subnets: []*armnetwork.Subnet{ { - ID: to.Ptr("/subscriptions/" + otherSubscriptionID + "/resourceGroups/" + otherResourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), + ID: new("/subscriptions/" + otherSubscriptionID + "/resourceGroups/" + otherResourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), }, }, }, @@ -330,19 +330,19 @@ func TestNetworkRouteTable(t *testing.T) { // Test route table with route that has NextHopIPAddress routeTableName := "test-route-table" routeTable := &armnetwork.RouteTable{ - Name: to.Ptr(routeTableName), - Location: to.Ptr("eastus"), + Name: new(routeTableName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armnetwork.RouteTablePropertiesFormat{ Routes: []*armnetwork.Route{ { - Name: to.Ptr("test-route"), + Name: new("test-route"), Properties: &armnetwork.RoutePropertiesFormat{ - AddressPrefix: to.Ptr("10.0.0.0/16"), - NextHopType: to.Ptr(armnetwork.RouteNextHopTypeVirtualAppliance), - NextHopIPAddress: to.Ptr("10.0.0.1"), + AddressPrefix: new("10.0.0.0/16"), + NextHopType: new(armnetwork.RouteNextHopTypeVirtualAppliance), + NextHopIPAddress: new("10.0.0.1"), }, }, }, @@ -385,18 +385,18 @@ func TestNetworkRouteTable(t *testing.T) { // Test route table with route that doesn't have NextHopIPAddress routeTableName := "test-route-table" routeTable := &armnetwork.RouteTable{ - Name: to.Ptr(routeTableName), - Location: to.Ptr("eastus"), + Name: new(routeTableName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armnetwork.RouteTablePropertiesFormat{ Routes: []*armnetwork.Route{ { - Name: to.Ptr("test-route"), + Name: new("test-route"), Properties: &armnetwork.RoutePropertiesFormat{ - AddressPrefix: to.Ptr("10.0.0.0/16"), - NextHopType: to.Ptr(armnetwork.RouteNextHopTypeInternet), + AddressPrefix: new("10.0.0.0/16"), + NextHopType: new(armnetwork.RouteNextHopTypeInternet), // No NextHopIPAddress }, }, @@ -439,13 +439,7 @@ func TestNetworkRouteTable(t *testing.T) { t.Error("Expected IAMPermissions to return at least one permission") } expectedPermission := "Microsoft.Network/routeTables/read" - found := false - for _, perm := range permissions { - if perm == expectedPermission { - found = true - break - } - } + found := slices.Contains(permissions, expectedPermission) if !found { t.Errorf("Expected IAMPermissions to include %s", expectedPermission) } @@ -485,7 +479,7 @@ func TestNetworkRouteTable(t *testing.T) { // Verify PredefinedRole // PredefinedRole is available on the wrapper, not the adapter // Use type assertion with interface{} to access the method - if roleInterface, ok := interface{}(wrapper).(interface{ PredefinedRole() string }); ok { + if roleInterface, ok := any(wrapper).(interface{ PredefinedRole() string }); ok { role := roleInterface.PredefinedRole() if role != "Reader" { t.Errorf("Expected PredefinedRole to be 'Reader', got %s", role) @@ -525,7 +519,7 @@ func (m *MockRouteTablesPager) More() bool { func (mr *MockRouteTablesPagerMockRecorder) More() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeOf((*MockRouteTablesPager)(nil).More)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeFor[func() bool]()) } func (m *MockRouteTablesPager) NextPage(ctx context.Context) (armnetwork.RouteTablesClientListResponse, error) { @@ -536,9 +530,9 @@ func (m *MockRouteTablesPager) NextPage(ctx context.Context) (armnetwork.RouteTa return ret0, ret1 } -func (mr *MockRouteTablesPagerMockRecorder) NextPage(ctx interface{}) *gomock.Call { +func (mr *MockRouteTablesPagerMockRecorder) NextPage(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeOf((*MockRouteTablesPager)(nil).NextPage), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeFor[func(ctx context.Context) (armnetwork.RouteTablesClientListResponse, error)](), ctx) } // createAzureRouteTable creates a mock Azure route table for testing @@ -547,28 +541,28 @@ func createAzureRouteTable(routeTableName string) *armnetwork.RouteTable { resourceGroup := "test-rg" return &armnetwork.RouteTable{ - Name: to.Ptr(routeTableName), - Location: to.Ptr("eastus"), + Name: new(routeTableName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armnetwork.RouteTablePropertiesFormat{ // Routes (child resources) Routes: []*armnetwork.Route{ { - Name: to.Ptr("test-route"), + Name: new("test-route"), Properties: &armnetwork.RoutePropertiesFormat{ - AddressPrefix: to.Ptr("10.0.0.0/16"), - NextHopType: to.Ptr(armnetwork.RouteNextHopTypeVirtualAppliance), - NextHopIPAddress: to.Ptr("10.0.0.1"), + AddressPrefix: new("10.0.0.0/16"), + NextHopType: new(armnetwork.RouteNextHopTypeVirtualAppliance), + NextHopIPAddress: new("10.0.0.1"), }, }, }, // Subnets (external resources) Subnets: []*armnetwork.Subnet{ { - ID: to.Ptr("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet"), }, }, }, diff --git a/sources/azure/manual/network-subnet_test.go b/sources/azure/manual/network-subnet_test.go index d6966a04..7e55e9d8 100644 --- a/sources/azure/manual/network-subnet_test.go +++ b/sources/azure/manual/network-subnet_test.go @@ -5,7 +5,6 @@ import ( "errors" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" @@ -200,7 +199,7 @@ func TestNetworkSubnet(t *testing.T) { { SubnetListResult: armnetwork.SubnetListResult{ Value: []*armnetwork.Subnet{ - {Name: nil, ID: to.Ptr("/some/id")}, + {Name: nil, ID: new("/some/id")}, validSubnet, }, }, @@ -268,11 +267,11 @@ func TestNetworkSubnet(t *testing.T) { func createAzureSubnet(subnetName, vnetName string) *armnetwork.Subnet { return &armnetwork.Subnet{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/" + vnetName + "/subnets/" + subnetName), - Name: to.Ptr(subnetName), - Type: to.Ptr("Microsoft.Network/virtualNetworks/subnets"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/" + vnetName + "/subnets/" + subnetName), + Name: new(subnetName), + Type: new("Microsoft.Network/virtualNetworks/subnets"), Properties: &armnetwork.SubnetPropertiesFormat{ - AddressPrefix: to.Ptr("10.0.0.0/24"), + AddressPrefix: new("10.0.0.0/24"), }, } } diff --git a/sources/azure/manual/network-virtual-network_test.go b/sources/azure/manual/network-virtual-network_test.go index 4b0c1c43..c25b2bc7 100644 --- a/sources/azure/manual/network-virtual-network_test.go +++ b/sources/azure/manual/network-virtual-network_test.go @@ -4,9 +4,9 @@ import ( "context" "errors" "reflect" + "slices" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" "go.uber.org/mock/gomock" @@ -208,13 +208,13 @@ func TestNetworkVirtualNetwork(t *testing.T) { vnet1 := createAzureVirtualNetwork("test-vnet-1") vnet2 := &armnetwork.VirtualNetwork{ Name: nil, // VNet with nil name should cause an error in azureVirtualNetworkToSDPItem - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armnetwork.VirtualNetworkPropertiesFormat{ AddressSpace: &armnetwork.AddressSpace{ - AddressPrefixes: []*string{to.Ptr("10.0.0.0/16")}, + AddressPrefixes: []*string{new("10.0.0.0/16")}, }, }, } @@ -313,13 +313,7 @@ func TestNetworkVirtualNetwork(t *testing.T) { t.Error("Expected IAMPermissions to return at least one permission") } expectedPermission := "Microsoft.Network/virtualNetworks/read" - found := false - for _, perm := range permissions { - if perm == expectedPermission { - found = true - break - } - } + found := slices.Contains(permissions, expectedPermission) if !found { t.Errorf("Expected IAMPermissions to include %s", expectedPermission) } @@ -391,7 +385,7 @@ func (m *MockVirtualNetworksPager) More() bool { func (mr *MockVirtualNetworksPagerMockRecorder) More() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeOf((*MockVirtualNetworksPager)(nil).More)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeFor[func() bool]()) } func (m *MockVirtualNetworksPager) NextPage(ctx context.Context) (armnetwork.VirtualNetworksClientListResponse, error) { @@ -402,29 +396,29 @@ func (m *MockVirtualNetworksPager) NextPage(ctx context.Context) (armnetwork.Vir return ret0, ret1 } -func (mr *MockVirtualNetworksPagerMockRecorder) NextPage(ctx interface{}) *gomock.Call { +func (mr *MockVirtualNetworksPagerMockRecorder) NextPage(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeOf((*MockVirtualNetworksPager)(nil).NextPage), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeFor[func(ctx context.Context) (armnetwork.VirtualNetworksClientListResponse, error)](), ctx) } // createAzureVirtualNetwork creates a mock Azure virtual network for testing func createAzureVirtualNetwork(vnetName string) *armnetwork.VirtualNetwork { return &armnetwork.VirtualNetwork{ - Name: to.Ptr(vnetName), - Location: to.Ptr("eastus"), + Name: new(vnetName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armnetwork.VirtualNetworkPropertiesFormat{ AddressSpace: &armnetwork.AddressSpace{ - AddressPrefixes: []*string{to.Ptr("10.0.0.0/16")}, + AddressPrefixes: []*string{new("10.0.0.0/16")}, }, Subnets: []*armnetwork.Subnet{ { - Name: to.Ptr("default"), + Name: new("default"), Properties: &armnetwork.SubnetPropertiesFormat{ - AddressPrefix: to.Ptr("10.0.0.0/24"), + AddressPrefix: new("10.0.0.0/24"), }, }, }, @@ -437,29 +431,29 @@ func createAzureVirtualNetwork(vnetName string) *armnetwork.VirtualNetwork { func createAzureVirtualNetworkWithDefaultNatGatewayAndDhcpOptions(vnetName, subscriptionID, resourceGroup string) *armnetwork.VirtualNetwork { natGatewayID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/natGateways/test-nat-gateway" return &armnetwork.VirtualNetwork{ - Name: to.Ptr(vnetName), - Location: to.Ptr("eastus"), + Name: new(vnetName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armnetwork.VirtualNetworkPropertiesFormat{ AddressSpace: &armnetwork.AddressSpace{ - AddressPrefixes: []*string{to.Ptr("10.0.0.0/16")}, + AddressPrefixes: []*string{new("10.0.0.0/16")}, }, DefaultPublicNatGateway: &armnetwork.SubResource{ - ID: to.Ptr(natGatewayID), + ID: new(natGatewayID), }, DhcpOptions: &armnetwork.DhcpOptions{ DNSServers: []*string{ - to.Ptr("10.0.0.1"), // IP address → stdlib.NetworkIP - to.Ptr("dns.internal"), // hostname → stdlib.NetworkDNS + new("10.0.0.1"), // IP address → stdlib.NetworkIP + new("dns.internal"), // hostname → stdlib.NetworkDNS }, }, Subnets: []*armnetwork.Subnet{ { - Name: to.Ptr("default"), + Name: new("default"), Properties: &armnetwork.SubnetPropertiesFormat{ - AddressPrefix: to.Ptr("10.0.0.0/24"), + AddressPrefix: new("10.0.0.0/24"), }, }, }, diff --git a/sources/azure/manual/network-zone_test.go b/sources/azure/manual/network-zone_test.go index c6a8d399..9c3a7a41 100644 --- a/sources/azure/manual/network-zone_test.go +++ b/sources/azure/manual/network-zone_test.go @@ -5,9 +5,9 @@ import ( "errors" "fmt" "reflect" + "slices" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" "go.uber.org/mock/gomock" @@ -121,7 +121,7 @@ func TestNetworkZone(t *testing.T) { // Let's test with a zone that has nil name which will cause an error zoneWithNilName := &armdns.Zone{ Name: nil, - Location: to.Ptr("eastus"), + Location: new("eastus"), Properties: &armdns.ZoneProperties{}, } @@ -231,9 +231,9 @@ func TestNetworkZone(t *testing.T) { zone1 := createAzureZone("example.com", subscriptionID, resourceGroup) zone2 := &armdns.Zone{ Name: nil, // Zone with nil name should be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armdns.ZoneProperties{}, } @@ -339,13 +339,7 @@ func TestNetworkZone(t *testing.T) { t.Error("Expected IAMPermissions to return at least one permission") } expectedPermission := "Microsoft.Network/dnszones/read" - found := false - for _, perm := range permissions { - if perm == expectedPermission { - found = true - break - } - } + found := slices.Contains(permissions, expectedPermission) if !found { t.Errorf("Expected IAMPermissions to include %s", expectedPermission) } @@ -405,15 +399,15 @@ func TestNetworkZone(t *testing.T) { // Test zone without virtual networks zoneName := "example.com" zone := &armdns.Zone{ - Name: to.Ptr(zoneName), - Location: to.Ptr("eastus"), + Name: new(zoneName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armdns.ZoneProperties{ NameServers: []*string{ - to.Ptr("ns1.example.com"), - to.Ptr("ns2.example.com"), + new("ns1.example.com"), + new("ns2.example.com"), }, }, } @@ -481,7 +475,7 @@ func (m *MockZonesPager) More() bool { func (mr *MockZonesPagerMockRecorder) More() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeOf((*MockZonesPager)(nil).More)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeFor[func() bool]()) } func (m *MockZonesPager) NextPage(ctx context.Context) (armdns.ZonesClientListByResourceGroupResponse, error) { @@ -492,9 +486,9 @@ func (m *MockZonesPager) NextPage(ctx context.Context) (armdns.ZonesClientListBy return ret0, ret1 } -func (mr *MockZonesPagerMockRecorder) NextPage(ctx interface{}) *gomock.Call { +func (mr *MockZonesPagerMockRecorder) NextPage(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeOf((*MockZonesPager)(nil).NextPage), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeFor[func(ctx context.Context) (armdns.ZonesClientListByResourceGroupResponse, error)](), ctx) } // createAzureZone creates a mock Azure DNS zone for testing with all linked resources @@ -503,27 +497,27 @@ func createAzureZone(zoneName, subscriptionID, resourceGroup string) *armdns.Zon resolutionVNetID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/test-res-vnet", subscriptionID, resourceGroup) return &armdns.Zone{ - Name: to.Ptr(zoneName), - Location: to.Ptr("eastus"), + Name: new(zoneName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armdns.ZoneProperties{ - MaxNumberOfRecordSets: to.Ptr(int64(5000)), - NumberOfRecordSets: to.Ptr(int64(10)), + MaxNumberOfRecordSets: new(int64(5000)), + NumberOfRecordSets: new(int64(10)), NameServers: []*string{ - to.Ptr("ns1.example.com"), - to.Ptr("ns2.example.com"), + new("ns1.example.com"), + new("ns2.example.com"), }, RegistrationVirtualNetworks: []*armdns.SubResource{ { - ID: to.Ptr(registrationVNetID), + ID: new(registrationVNetID), }, }, ResolutionVirtualNetworks: []*armdns.SubResource{ { - ID: to.Ptr(resolutionVNetID), + ID: new(resolutionVNetID), }, }, }, @@ -535,20 +529,20 @@ func createAzureZoneWithDifferentScopeVNet(zoneName, subscriptionID, resourceGro registrationVNetID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/virtualNetworks/test-reg-vnet", otherSubscriptionID, otherResourceGroup) return &armdns.Zone{ - Name: to.Ptr(zoneName), - Location: to.Ptr("eastus"), + Name: new(zoneName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armdns.ZoneProperties{ - MaxNumberOfRecordSets: to.Ptr(int64(5000)), - NumberOfRecordSets: to.Ptr(int64(10)), + MaxNumberOfRecordSets: new(int64(5000)), + NumberOfRecordSets: new(int64(10)), NameServers: []*string{ - to.Ptr("ns1.example.com"), + new("ns1.example.com"), }, RegistrationVirtualNetworks: []*armdns.SubResource{ { - ID: to.Ptr(registrationVNetID), + ID: new(registrationVNetID), }, }, }, diff --git a/sources/azure/manual/sql-database_test.go b/sources/azure/manual/sql-database_test.go index e043a58f..11e4eec3 100644 --- a/sources/azure/manual/sql-database_test.go +++ b/sources/azure/manual/sql-database_test.go @@ -3,9 +3,9 @@ package manual_test import ( "context" "errors" + "slices" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" "go.uber.org/mock/gomock" @@ -244,13 +244,13 @@ func TestSqlDatabase(t *testing.T) { database1 := createAzureSqlDatabase(serverName, "database-1", "") database2 := &armsql.Database{ Name: nil, // Database with nil name should be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/databases/database-2"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-server/databases/database-2"), Properties: &armsql.DatabaseProperties{ - Status: to.Ptr(armsql.DatabaseStatusOnline), + Status: new(armsql.DatabaseStatusOnline), }, } @@ -363,13 +363,7 @@ func TestSqlDatabase(t *testing.T) { t.Error("Expected IAMPermissions to return at least one permission") } expectedPermission := "Microsoft.Sql/servers/databases/read" - found := false - for _, perm := range permissions { - if perm == expectedPermission { - found = true - break - } - } + found := slices.Contains(permissions, expectedPermission) if !found { t.Errorf("Expected IAMPermissions to include %s", expectedPermission) } @@ -410,20 +404,20 @@ func createAzureSqlDatabase(serverName, databaseName, elasticPoolID string) *arm databaseID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Sql/servers/" + serverName + "/databases/" + databaseName db := &armsql.Database{ - Name: to.Ptr(databaseName), - Location: to.Ptr("eastus"), + Name: new(databaseName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, - ID: to.Ptr(databaseID), + ID: new(databaseID), Properties: &armsql.DatabaseProperties{ - Status: to.Ptr(armsql.DatabaseStatusOnline), + Status: new(armsql.DatabaseStatusOnline), }, } if elasticPoolID != "" { - db.Properties.ElasticPoolID = to.Ptr(elasticPoolID) + db.Properties.ElasticPoolID = new(elasticPoolID) } return db diff --git a/sources/azure/manual/sql-server-firewall-rule_test.go b/sources/azure/manual/sql-server-firewall-rule_test.go index e6ce9390..852d889b 100644 --- a/sources/azure/manual/sql-server-firewall-rule_test.go +++ b/sources/azure/manual/sql-server-firewall-rule_test.go @@ -3,9 +3,9 @@ package manual_test import ( "context" "errors" + "slices" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" "go.uber.org/mock/gomock" @@ -172,7 +172,7 @@ func TestSqlServerFirewallRule(t *testing.T) { testClient := &testSqlServerFirewallRuleClient{ MockSqlServerFirewallRuleClient: mockClient, - pager: pager, + pager: pager, } wrapper := manual.NewSqlServerFirewallRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -207,7 +207,7 @@ func TestSqlServerFirewallRule(t *testing.T) { testClient := &testSqlServerFirewallRuleClient{ MockSqlServerFirewallRuleClient: mockClient, - pager: pager, + pager: pager, } wrapper := manual.NewSqlServerFirewallRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -261,7 +261,7 @@ func TestSqlServerFirewallRule(t *testing.T) { errorPager := &errorSqlServerFirewallRulePager{} testClient := &testSqlServerFirewallRuleClient{ MockSqlServerFirewallRuleClient: mockClient, - pager: errorPager, + pager: errorPager, } wrapper := manual.NewSqlServerFirewallRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) @@ -281,13 +281,7 @@ func TestSqlServerFirewallRule(t *testing.T) { t.Error("Expected IAMPermissions to return at least one permission") } expectedPermission := "Microsoft.Sql/servers/firewallRules/read" - found := false - for _, perm := range permissions { - if perm == expectedPermission { - found = true - break - } - } + found := slices.Contains(permissions, expectedPermission) if !found { t.Errorf("Expected IAMPermissions to include %s", expectedPermission) } @@ -320,11 +314,11 @@ func TestSqlServerFirewallRule(t *testing.T) { func createAzureSqlServerFirewallRule(serverName, firewallRuleName string) *armsql.FirewallRule { ruleID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Sql/servers/" + serverName + "/firewallRules/" + firewallRuleName return &armsql.FirewallRule{ - Name: to.Ptr(firewallRuleName), - ID: to.Ptr(ruleID), + Name: new(firewallRuleName), + ID: new(ruleID), Properties: &armsql.ServerFirewallRuleProperties{ - StartIPAddress: to.Ptr("0.0.0.0"), - EndIPAddress: to.Ptr("255.255.255.255"), + StartIPAddress: new("0.0.0.0"), + EndIPAddress: new("255.255.255.255"), }, } } diff --git a/sources/azure/manual/sql-server_test.go b/sources/azure/manual/sql-server_test.go index d2165407..9e1ad621 100644 --- a/sources/azure/manual/sql-server_test.go +++ b/sources/azure/manual/sql-server_test.go @@ -3,10 +3,10 @@ package manual_test import ( "context" "errors" + "slices" "sync" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" "go.uber.org/mock/gomock" @@ -376,14 +376,14 @@ func TestSqlServer(t *testing.T) { { Properties: &armsql.PrivateEndpointConnectionProperties{ PrivateEndpoint: &armsql.PrivateEndpointProperty{ - ID: to.Ptr(privateEndpointID1), + ID: new(privateEndpointID1), }, }, }, { Properties: &armsql.PrivateEndpointConnectionProperties{ PrivateEndpoint: &armsql.PrivateEndpointProperty{ - ID: to.Ptr(privateEndpointID2), + ID: new(privateEndpointID2), }, }, }, @@ -638,12 +638,12 @@ func TestSqlServer(t *testing.T) { server1 := createAzureSqlServer("server-1", "", "") server2 := &armsql.Server{ Name: nil, // Server with nil name should be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armsql.ServerProperties{ - Version: to.Ptr("12.0"), + Version: new("12.0"), }, } @@ -838,20 +838,14 @@ func TestSqlServer(t *testing.T) { t.Error("Expected IAMPermissions to return at least one permission") } expectedPermission := "Microsoft.Sql/servers/read" - found := false - for _, perm := range permissions { - if perm == expectedPermission { - found = true - break - } - } + found := slices.Contains(permissions, expectedPermission) if !found { t.Errorf("Expected IAMPermissions to include %s", expectedPermission) } // Verify PredefinedRole // PredefinedRole is available on the wrapper, not the adapter - if roleInterface, ok := interface{}(wrapper).(interface{ PredefinedRole() string }); ok { + if roleInterface, ok := any(wrapper).(interface{ PredefinedRole() string }); ok { role := roleInterface.PredefinedRole() if role != "Reader" { t.Errorf("Expected PredefinedRole to be 'Reader', got %s", role) @@ -902,27 +896,27 @@ func createAzureSqlServer(serverName, primaryUserAssignedIdentityID, fullyQualif serverID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Sql/servers/" + serverName server := &armsql.Server{ - Name: to.Ptr(serverName), - Location: to.Ptr("eastus"), + Name: new(serverName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, - ID: to.Ptr(serverID), + ID: new(serverID), Properties: &armsql.ServerProperties{ - Version: to.Ptr("12.0"), - AdministratorLogin: to.Ptr("admin"), - FullyQualifiedDomainName: to.Ptr(fullyQualifiedDomainName), + Version: new("12.0"), + AdministratorLogin: new("admin"), + FullyQualifiedDomainName: new(fullyQualifiedDomainName), }, } if primaryUserAssignedIdentityID != "" { - server.Properties.PrimaryUserAssignedIdentityID = to.Ptr(primaryUserAssignedIdentityID) + server.Properties.PrimaryUserAssignedIdentityID = new(primaryUserAssignedIdentityID) } if fullyQualifiedDomainName == "" && serverName != "" { // Set a default FQDN if not provided but server name is set - server.Properties.FullyQualifiedDomainName = to.Ptr(serverName + ".database.windows.net") + server.Properties.FullyQualifiedDomainName = new(serverName + ".database.windows.net") } return server @@ -933,7 +927,7 @@ func createAzureSqlServerWithUserAssignedIdentities(serverName, primaryUserAssig server := createAzureSqlServer(serverName, primaryUserAssignedIdentityID, fullyQualifiedDomainName) if userAssignedIdentities != nil { server.Identity = &armsql.ResourceIdentity{ - Type: to.Ptr(armsql.IdentityTypeUserAssigned), + Type: new(armsql.IdentityTypeUserAssigned), UserAssignedIdentities: userAssignedIdentities, } } @@ -953,7 +947,7 @@ func createAzureSqlServerWithPrivateEndpointConnections(serverName, primaryUserA func createAzureSqlServerWithKeyId(serverName, primaryUserAssignedIdentityID, fullyQualifiedDomainName, keyID string) *armsql.Server { server := createAzureSqlServer(serverName, primaryUserAssignedIdentityID, fullyQualifiedDomainName) if keyID != "" { - server.Properties.KeyID = to.Ptr(keyID) + server.Properties.KeyID = new(keyID) } return server } diff --git a/sources/azure/manual/storage-account_test.go b/sources/azure/manual/storage-account_test.go index 99a77918..3dac2006 100644 --- a/sources/azure/manual/storage-account_test.go +++ b/sources/azure/manual/storage-account_test.go @@ -5,7 +5,6 @@ import ( "errors" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" @@ -194,12 +193,12 @@ func TestStorageAccount(t *testing.T) { account1 := createAzureStorageAccount("teststorageaccount1", "Succeeded") account2 := &armstorage.Account{ Name: nil, // Account with nil name should be skipped - Location: to.Ptr("eastus"), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), + "env": new("test"), }, Properties: &armstorage.AccountProperties{ - ProvisioningState: to.Ptr(armstorage.ProvisioningStateSucceeded), + ProvisioningState: new(armstorage.ProvisioningStateSucceeded), }, } @@ -297,20 +296,20 @@ func TestStorageAccount(t *testing.T) { func createAzureStorageAccount(accountName, provisioningState string) *armstorage.Account { state := armstorage.ProvisioningState(provisioningState) return &armstorage.Account{ - Name: to.Ptr(accountName), - Location: to.Ptr("eastus"), - Kind: to.Ptr(armstorage.KindStorageV2), + Name: new(accountName), + Location: new("eastus"), + Kind: new(armstorage.KindStorageV2), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armstorage.AccountProperties{ ProvisioningState: &state, PrimaryEndpoints: &armstorage.Endpoints{ - Blob: to.Ptr("https://" + accountName + ".blob.core.windows.net/"), - Queue: to.Ptr("https://" + accountName + ".queue.core.windows.net/"), - Table: to.Ptr("https://" + accountName + ".table.core.windows.net/"), - File: to.Ptr("https://" + accountName + ".file.core.windows.net/"), + Blob: new("https://" + accountName + ".blob.core.windows.net/"), + Queue: new("https://" + accountName + ".queue.core.windows.net/"), + Table: new("https://" + accountName + ".table.core.windows.net/"), + File: new("https://" + accountName + ".file.core.windows.net/"), }, }, } diff --git a/sources/azure/manual/storage-blob-container_test.go b/sources/azure/manual/storage-blob-container_test.go index 86a391be..6d392582 100644 --- a/sources/azure/manual/storage-blob-container_test.go +++ b/sources/azure/manual/storage-blob-container_test.go @@ -6,7 +6,6 @@ import ( "fmt" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" @@ -325,9 +324,9 @@ func TestStorageBlobContainer(t *testing.T) { Name: nil, }, { - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/blobServices/default/containers/valid-container"), - Name: to.Ptr("valid-container"), - Type: to.Ptr("Microsoft.Storage/storageAccounts/blobServices/containers"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/blobServices/default/containers/valid-container"), + Name: new("valid-container"), + Type: new("Microsoft.Storage/storageAccounts/blobServices/containers"), }, }, }, @@ -411,27 +410,27 @@ func TestStorageBlobContainer(t *testing.T) { // createAzureBlobContainer creates a mock Azure blob container for testing func createAzureBlobContainer(containerName string) *armstorage.BlobContainer { return &armstorage.BlobContainer{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/blobServices/default/containers/" + containerName), - Name: to.Ptr(containerName), - Type: to.Ptr("Microsoft.Storage/storageAccounts/blobServices/containers"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/blobServices/default/containers/" + containerName), + Name: new(containerName), + Type: new("Microsoft.Storage/storageAccounts/blobServices/containers"), ContainerProperties: &armstorage.ContainerProperties{ - PublicAccess: to.Ptr(armstorage.PublicAccessNone), + PublicAccess: new(armstorage.PublicAccessNone), }, - Etag: to.Ptr("\"0x8D1234567890ABC\""), + Etag: new("\"0x8D1234567890ABC\""), } } // createAzureBlobContainerWithEncryptionScope creates a mock Azure blob container with a default encryption scope func createAzureBlobContainerWithEncryptionScope(containerName, encryptionScopeName string) *armstorage.BlobContainer { return &armstorage.BlobContainer{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/blobServices/default/containers/" + containerName), - Name: to.Ptr(containerName), - Type: to.Ptr("Microsoft.Storage/storageAccounts/blobServices/containers"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/blobServices/default/containers/" + containerName), + Name: new(containerName), + Type: new("Microsoft.Storage/storageAccounts/blobServices/containers"), ContainerProperties: &armstorage.ContainerProperties{ - PublicAccess: to.Ptr(armstorage.PublicAccessNone), - DefaultEncryptionScope: to.Ptr(encryptionScopeName), - DenyEncryptionScopeOverride: to.Ptr(false), + PublicAccess: new(armstorage.PublicAccessNone), + DefaultEncryptionScope: new(encryptionScopeName), + DenyEncryptionScopeOverride: new(false), }, - Etag: to.Ptr("\"0x8D1234567890ABC\""), + Etag: new("\"0x8D1234567890ABC\""), } } diff --git a/sources/azure/manual/storage-fileshare_test.go b/sources/azure/manual/storage-fileshare_test.go index a29503b0..f810dfdb 100644 --- a/sources/azure/manual/storage-fileshare_test.go +++ b/sources/azure/manual/storage-fileshare_test.go @@ -5,7 +5,6 @@ import ( "errors" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" @@ -241,9 +240,9 @@ func TestStorageFileShare(t *testing.T) { Name: nil, }, { - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/fileServices/default/shares/valid-share"), - Name: to.Ptr("valid-share"), - Type: to.Ptr("Microsoft.Storage/storageAccounts/fileServices/shares"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/fileServices/default/shares/valid-share"), + Name: new("valid-share"), + Type: new("Microsoft.Storage/storageAccounts/fileServices/shares"), }, }, }, @@ -327,13 +326,13 @@ func TestStorageFileShare(t *testing.T) { // createAzureFileShare creates a mock Azure file share for testing func createAzureFileShare(shareName string) *armstorage.FileShare { return &armstorage.FileShare{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/fileServices/default/shares/" + shareName), - Name: to.Ptr(shareName), - Type: to.Ptr("Microsoft.Storage/storageAccounts/fileServices/shares"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/fileServices/default/shares/" + shareName), + Name: new(shareName), + Type: new("Microsoft.Storage/storageAccounts/fileServices/shares"), FileShareProperties: &armstorage.FileShareProperties{ - AccessTier: to.Ptr(armstorage.ShareAccessTierHot), - ShareQuota: to.Ptr(int32(5120)), // 5GB + AccessTier: new(armstorage.ShareAccessTierHot), + ShareQuota: new(int32(5120)), // 5GB }, - Etag: to.Ptr("\"0x8D1234567890ABC\""), + Etag: new("\"0x8D1234567890ABC\""), } } diff --git a/sources/azure/manual/storage-queues_test.go b/sources/azure/manual/storage-queues_test.go index ad631a15..55c380c4 100644 --- a/sources/azure/manual/storage-queues_test.go +++ b/sources/azure/manual/storage-queues_test.go @@ -5,7 +5,6 @@ import ( "errors" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" @@ -393,13 +392,13 @@ func TestStorageQueues(t *testing.T) { // createAzureQueue creates a mock Azure queue for testing func createAzureQueue(queueName string) *armstorage.Queue { return &armstorage.Queue{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/queueServices/default/queues/" + queueName), - Name: to.Ptr(queueName), - Type: to.Ptr("Microsoft.Storage/storageAccounts/queueServices/queues"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/queueServices/default/queues/" + queueName), + Name: new(queueName), + Type: new("Microsoft.Storage/storageAccounts/queueServices/queues"), QueueProperties: &armstorage.QueueProperties{ Metadata: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, }, } diff --git a/sources/azure/manual/storage-table_test.go b/sources/azure/manual/storage-table_test.go index 86d1b2ad..181f872c 100644 --- a/sources/azure/manual/storage-table_test.go +++ b/sources/azure/manual/storage-table_test.go @@ -3,9 +3,9 @@ package manual_test import ( "context" "errors" + "slices" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" "go.uber.org/mock/gomock" @@ -394,13 +394,7 @@ func TestStorageTables(t *testing.T) { } expectedPermission := "Microsoft.Storage/storageAccounts/tableServices/tables/read" - found := false - for _, perm := range permissions { - if perm == expectedPermission { - found = true - break - } - } + found := slices.Contains(permissions, expectedPermission) if !found { t.Errorf("Expected IAMPermissions to include %s", expectedPermission) } @@ -410,9 +404,9 @@ func TestStorageTables(t *testing.T) { // createAzureTable creates a mock Azure table for testing func createAzureTable(tableName string) *armstorage.Table { return &armstorage.Table{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/tableServices/default/tables/" + tableName), - Name: to.Ptr(tableName), - Type: to.Ptr("Microsoft.Storage/storageAccounts/tableServices/tables"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/tableServices/default/tables/" + tableName), + Name: new(tableName), + Type: new("Microsoft.Storage/storageAccounts/tableServices/tables"), TableProperties: &armstorage.TableProperties{}, } } diff --git a/sources/azure/proc/proc.go b/sources/azure/proc/proc.go index 2fddfca4..db06900c 100644 --- a/sources/azure/proc/proc.go +++ b/sources/azure/proc/proc.go @@ -132,7 +132,7 @@ func InitializeAdapters(ctx context.Context, engine *discovery.Engine, cfg *Azur } // TODO: Implement linker when Azure dynamic adapters are available - var linker interface{} = nil + var linker any = nil discoveryAdapters, err := adapters(ctx, cfg.SubscriptionID, cfg.TenantID, cfg.ClientID, cfg.Regions, cred, linker, true, sharedCache) if err != nil { @@ -206,7 +206,7 @@ func adapters( clientID string, regions []string, cred *azidentity.DefaultAzureCredential, - linker interface{}, // TODO: Use *azureshared.Linker when azureshared package is fully implemented + linker any, // TODO: Use *azureshared.Linker when azureshared package is fully implemented initAzureClients bool, cache sdpcache.Cache, ) ([]discovery.Adapter, error) { diff --git a/sources/azure/shared/resource_id_item_type.go b/sources/azure/shared/resource_id_item_type.go index 58c9b249..a1db76c9 100644 --- a/sources/azure/shared/resource_id_item_type.go +++ b/sources/azure/shared/resource_id_item_type.go @@ -9,7 +9,7 @@ import ( // item types (see models.go). Enables generated linked queries to match existing adapter // naming: azure-{api}-{resource} with kebab-case resource. var azureProviderToAPI = map[string]string{ - "microsoft.compute": "compute", + "microsoft.compute": "compute", "microsoft.network": "network", "microsoft.storage": "storage", "microsoft.sql": "sql", @@ -17,7 +17,7 @@ var azureProviderToAPI = map[string]string{ "microsoft.keyvault": "keyvault", "microsoft.managedidentity": "managedidentity", "microsoft.batch": "batch", - "microsoft.dbforpostgresql": "dbforpostgresql", + "microsoft.dbforpostgresql": "dbforpostgresql", "microsoft.elasticsan": "elasticsan", "microsoft.authorization": "authorization", "microsoft.maintenance": "maintenance", @@ -56,15 +56,15 @@ func SingularizeResourceType(kebab string) string { return kebab } // -ies -> -y (e.g. galleries -> gallery, user-assigned-identities -> user-assigned-identity) - if strings.HasSuffix(kebab, "ies") { - return strings.TrimSuffix(kebab, "ies") + "y" + if before, ok := strings.CutSuffix(kebab, "ies"); ok { + return before + "y" } // -addresses -> -address (e.g. public-ip-addresses -> public-ip-address) - if strings.HasSuffix(kebab, "addresses") { - return strings.TrimSuffix(kebab, "addresses") + "address" + if before, ok := strings.CutSuffix(kebab, "addresses"); ok { + return before + "address" } - if strings.HasSuffix(kebab, "s") { - return strings.TrimSuffix(kebab, "s") + if before, ok := strings.CutSuffix(kebab, "s"); ok { + return before } return kebab } diff --git a/sources/azure/shared/utils_test.go b/sources/azure/shared/utils_test.go index 21126095..0fc91eaa 100644 --- a/sources/azure/shared/utils_test.go +++ b/sources/azure/shared/utils_test.go @@ -256,9 +256,9 @@ func TestConvertAzureTags(t *testing.T) { { name: "valid tags with values", azureTags: map[string]*string{ - "env": stringPtr("production"), - "project": stringPtr("overmind"), - "team": stringPtr("platform"), + "env": new("production"), + "project": new("overmind"), + "team": new("platform"), }, expected: map[string]string{ "env": "production", @@ -279,9 +279,9 @@ func TestConvertAzureTags(t *testing.T) { { name: "tags with nil values - should be skipped", azureTags: map[string]*string{ - "env": stringPtr("production"), + "env": new("production"), "project": nil, - "team": stringPtr("platform"), + "team": new("platform"), }, expected: map[string]string{ "env": "production", @@ -300,7 +300,7 @@ func TestConvertAzureTags(t *testing.T) { { name: "single tag", azureTags: map[string]*string{ - "env": stringPtr("test"), + "env": new("test"), }, expected: map[string]string{ "env": "test", @@ -309,8 +309,8 @@ func TestConvertAzureTags(t *testing.T) { { name: "tags with empty string values", azureTags: map[string]*string{ - "env": stringPtr(""), - "project": stringPtr("overmind"), + "env": new(""), + "project": new("overmind"), }, expected: map[string]string{ "env": "", @@ -320,8 +320,8 @@ func TestConvertAzureTags(t *testing.T) { { name: "tags with special characters", azureTags: map[string]*string{ - "tag-with-dashes": stringPtr("value_with_underscores"), - "tag.with.dots": stringPtr("value with spaces"), + "tag-with-dashes": new("value_with_underscores"), + "tag.with.dots": new("value with spaces"), }, expected: map[string]string{ "tag-with-dashes": "value_with_underscores", @@ -340,11 +340,6 @@ func TestConvertAzureTags(t *testing.T) { } } -// stringPtr is a helper function to create a pointer to a string -func stringPtr(s string) *string { - return &s -} - func TestExtractSQLServerNameFromDatabaseID(t *testing.T) { tests := []struct { name string diff --git a/sources/gcp/dynamic/adapter.go b/sources/gcp/dynamic/adapter.go index 9b920ec9..5da9c03b 100644 --- a/sources/gcp/dynamic/adapter.go +++ b/sources/gcp/dynamic/adapter.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "slices" "buf.build/go/protovalidate" log "github.com/sirupsen/logrus" @@ -11,8 +12,8 @@ import ( "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" + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) @@ -104,10 +105,8 @@ func (g Adapter) validateScope(scope string) (gcpshared.LocationInfo, error) { } } - for _, validLoc := range g.locations { - if requestedLoc.Equals(validLoc) { - return requestedLoc, nil - } + if slices.ContainsFunc(g.locations, requestedLoc.Equals) { + return requestedLoc, nil } return gcpshared.LocationInfo{}, &sdp.QueryError{ ErrorType: sdp.QueryError_NOSCOPE, 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 c389490f..c46f1e0c 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 @@ -257,7 +257,7 @@ func TestAIPlatformBatchPredictionJob(t *testing.T) { if err != nil { t.Fatalf("Failed to get 'inputConfig' attribute: %v", err) } - inputConfigMap, ok := inputConfig.(map[string]interface{}) + inputConfigMap, ok := inputConfig.(map[string]any) if !ok { t.Fatalf("Expected inputConfig to be a map[string]interface{}, got %T", inputConfig) } @@ -270,7 +270,7 @@ func TestAIPlatformBatchPredictionJob(t *testing.T) { if err != nil { t.Fatalf("Failed to get 'outputConfig' attribute: %v", err) } - outputConfigMap, ok := outputConfig.(map[string]interface{}) + outputConfigMap, ok := outputConfig.(map[string]any) if !ok { t.Fatalf("Expected outputConfig to be a map[string]interface{}, got %T", outputConfig) } @@ -283,7 +283,7 @@ func TestAIPlatformBatchPredictionJob(t *testing.T) { if err != nil { t.Fatalf("Failed to get 'encryptionSpec' attribute: %v", err) } - encryptionSpecMap, ok := encryptionSpec.(map[string]interface{}) + encryptionSpecMap, ok := encryptionSpec.(map[string]any) if !ok { t.Fatalf("Expected encryptionSpec to be a map[string]interface{}, got %T", encryptionSpec) } 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 ae50dcb7..1da39ce2 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-custom-job_test.go @@ -121,7 +121,7 @@ func TestAIPlatformCustomJob(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://aiplatform.googleapis.com/v1/projects/%s/locations/global/customJobs/%s", projectID, jobID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Custom job not found"}, + Body: map[string]any{"error": "Custom job not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go b/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go index df9170ce..ae983c8e 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-endpoint_test.go @@ -198,7 +198,7 @@ func TestAIPlatformEndpoint(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://aiplatform.googleapis.com/v1/projects/%s/locations/global/endpoints/%s", projectID, endpointName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Endpoint not found"}, + Body: map[string]any{"error": "Endpoint not found"}, }, } 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 d761c5af..acc2f1ca 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 @@ -252,7 +252,7 @@ func TestAIPlatformModelDeploymentMonitoringJob(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://aiplatform.googleapis.com/v1/projects/%s/locations/%s/modelDeploymentMonitoringJobs/%s", projectID, location, jobName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Monitoring job not found"}, + Body: map[string]any{"error": "Monitoring job not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/ai-platform-model_test.go b/sources/gcp/dynamic/adapters/ai-platform-model_test.go index 5034f43f..a6c71567 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-model_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-model_test.go @@ -180,7 +180,7 @@ func TestAIPlatformModel(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://aiplatform.googleapis.com/v1/projects/%s/locations/global/models/%s", projectID, modelName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Model not found"}, + Body: map[string]any{"error": "Model not found"}, }, } 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 3e2cd358..05fff8b4 100644 --- a/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go +++ b/sources/gcp/dynamic/adapters/ai-platform-pipeline-job_test.go @@ -119,7 +119,7 @@ func TestAIPlatformPipelineJob(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://aiplatform.googleapis.com/v1/projects/%s/locations/global/pipelineJobs/%s", projectID, jobID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Pipeline job not found"}, + Body: map[string]any{"error": "Pipeline job not found"}, }, } 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 b35e51fe..a5b7caa2 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 @@ -266,7 +266,7 @@ func TestBigQueryDataTransferTransferConfig(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://bigquerydatatransfer.googleapis.com/v1/projects/%s/locations/%s/transferConfigs/%s", projectID, location, transferConfigName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Resource not found"}, + Body: map[string]any{"error": "Resource not found"}, }, } 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 2cd25def..67ca3253 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 @@ -187,7 +187,7 @@ func TestBigTableAdminAppProfile(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://bigtableadmin.googleapis.com/v2/projects/%s/instances/%s/appProfiles/%s", projectID, instanceName, appProfileID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "App profile not found"}, + Body: map[string]any{"error": "App profile not found"}, }, } 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 c1c48ec8..d9aa9277 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-backup_test.go @@ -130,7 +130,7 @@ func TestBigTableAdminBackup(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://bigtableadmin.googleapis.com/v2/projects/%s/instances/%s/clusters/%s/backups/%s", projectID, instanceName, clusterName, backupID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Backup not found"}, + Body: map[string]any{"error": "Backup not found"}, }, } 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 51eadc8f..234d5a42 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-cluster_test.go @@ -156,7 +156,7 @@ func TestBigTableAdminCluster(t *testing.T) { if err != nil { t.Fatalf("Failed to get 'encryptionConfig' attribute: %v", err) } - encryptionConfig, ok := val.(map[string]interface{}) + encryptionConfig, ok := val.(map[string]any) if !ok { t.Fatalf("Expected encryptionConfig to be a map[string]interface{}, got %T", val) } @@ -259,7 +259,7 @@ func TestBigTableAdminCluster(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://bigtableadmin.googleapis.com/v2/projects/%s/instances/%s/clusters/%s", projectID, instanceName, clusterName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Cluster not found"}, + Body: map[string]any{"error": "Cluster not found"}, }, } 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 2e0fa848..8e25d919 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-instance_test.go @@ -110,7 +110,7 @@ func TestBigTableAdminInstance(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://bigtableadmin.googleapis.com/v2/projects/%s/instances/%s", projectID, instanceName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Instance not found"}, + Body: map[string]any{"error": "Instance not found"}, }, } 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 a8615272..49f24d14 100644 --- a/sources/gcp/dynamic/adapters/big-table-admin-table_test.go +++ b/sources/gcp/dynamic/adapters/big-table-admin-table_test.go @@ -156,7 +156,7 @@ func TestBigTableAdminTable(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://bigtableadmin.googleapis.com/v2/projects/%s/instances/%s/tables/%s", projectID, instanceName, tableName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Table not found"}, + Body: map[string]any{"error": "Table not found"}, }, } 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 a94ee7d9..0a64ed57 100644 --- a/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go +++ b/sources/gcp/dynamic/adapters/cloud-billing-billing-info_test.go @@ -59,7 +59,7 @@ func TestCloudBillingBillingInfo(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://cloudbilling.googleapis.com/v1/projects/%s/billingInfo", projectID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Billing info not found"}, + Body: map[string]any{"error": "Billing info not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/cloud-build-build_test.go b/sources/gcp/dynamic/adapters/cloud-build-build_test.go index ca64382a..0fd42a0b 100644 --- a/sources/gcp/dynamic/adapters/cloud-build-build_test.go +++ b/sources/gcp/dynamic/adapters/cloud-build-build_test.go @@ -114,7 +114,7 @@ func TestCloudBuildBuild(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://cloudbuild.googleapis.com/v1/projects/%s/builds/%s", projectID, buildID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Build not found"}, + Body: map[string]any{"error": "Build not found"}, }, } 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 c542ead7..3c7a2f46 100644 --- a/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go +++ b/sources/gcp/dynamic/adapters/cloud-resource-manager-project_test.go @@ -56,7 +56,7 @@ func TestCloudResourceManagerProject(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://cloudresourcemanager.googleapis.com/v3/projects/%s", projectID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Project not found"}, + Body: map[string]any{"error": "Project not found"}, }, } 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 51fd89cb..ff3ad560 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 @@ -175,7 +175,7 @@ func TestCloudResourceManagerTagKey(t *testing.T) { if err != nil { t.Fatalf("Failed to get 'purposeData' attribute: %v", err) } - purposeData, ok := val.(map[string]interface{}) + purposeData, ok := val.(map[string]any) if !ok { t.Fatalf("Expected purposeData to be a map, got %T", val) } @@ -235,7 +235,7 @@ func TestCloudResourceManagerTagKey(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://cloudresourcemanager.googleapis.com/v3/tagKeys/%s", tagKeyID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": map[string]interface{}{"code": 404, "message": "TagKey not found"}}, + Body: map[string]any{"error": map[string]any{"code": 404, "message": "TagKey not found"}}, }, } 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 7ad034db..ea844d9c 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 @@ -113,7 +113,7 @@ func TestCloudResourceManagerTagValue(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://cloudresourcemanager.googleapis.com/v3/tagValues/%s", tagValueID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Tag value not found"}, + Body: map[string]any{"error": "Tag value not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go b/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go index bd203dfd..7f05bcf1 100644 --- a/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go +++ b/sources/gcp/dynamic/adapters/cloudfunctions-function_test.go @@ -176,7 +176,7 @@ func TestCloudFunctionsFunction(t *testing.T) { if err != nil { t.Fatalf("Failed to get 'buildConfig' attribute: %v", err) } - buildConfigMap, ok := buildConfig.(map[string]interface{}) + buildConfigMap, ok := buildConfig.(map[string]any) if !ok { t.Fatalf("Expected buildConfig to be a map, got %T", buildConfig) } @@ -316,7 +316,7 @@ func TestCloudFunctionsFunction(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://cloudfunctions.googleapis.com/v2/projects/%s/locations/%s/functions/%s", projectID, location, functionName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Function not found"}, + Body: map[string]any{"error": "Function not found"}, }, } 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 be2726ec..7ab06d43 100644 --- a/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go +++ b/sources/gcp/dynamic/adapters/compute-external-vpn-gateway_test.go @@ -117,7 +117,7 @@ func TestComputeExternalVpnGateway(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/externalVpnGateways/%s", projectID, gatewayName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Gateway not found"}, + Body: map[string]any{"error": "Gateway not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-firewall_test.go b/sources/gcp/dynamic/adapters/compute-firewall_test.go index aaddcf5d..1a3ba07e 100644 --- a/sources/gcp/dynamic/adapters/compute-firewall_test.go +++ b/sources/gcp/dynamic/adapters/compute-firewall_test.go @@ -130,7 +130,7 @@ func TestComputeFirewall(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/firewalls/%s", projectID, firewallName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Firewall not found"}, + Body: map[string]any{"error": "Firewall not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-global-address_test.go b/sources/gcp/dynamic/adapters/compute-global-address_test.go index 7fccd453..b61954f9 100644 --- a/sources/gcp/dynamic/adapters/compute-global-address_test.go +++ b/sources/gcp/dynamic/adapters/compute-global-address_test.go @@ -24,42 +24,42 @@ func TestComputeGlobalAddress(t *testing.T) { addressName := "test-global-address" globalAddress := &computepb.Address{ Name: &addressName, - Description: stringPtr("Test global address for load balancer"), - Address: stringPtr("203.0.113.12"), - AddressType: stringPtr("EXTERNAL"), - Status: stringPtr("RESERVED"), - Network: stringPtr("global/networks/test-network"), + Description: new("Test global address for load balancer"), + Address: new("203.0.113.12"), + AddressType: new("EXTERNAL"), + Status: new("RESERVED"), + Network: new("global/networks/test-network"), Labels: map[string]string{ "env": "test", "team": "networking", }, - Region: stringPtr("global"), - NetworkTier: stringPtr("PREMIUM"), - CreationTimestamp: stringPtr("2023-01-15T10:30:00.000-08:00"), - Id: uint64Ptr(1234567890123456789), - Kind: stringPtr("compute#globalAddress"), - SelfLink: stringPtr(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/addresses/%s", projectID, addressName)), + Region: new("global"), + NetworkTier: new("PREMIUM"), + CreationTimestamp: new("2023-01-15T10:30:00.000-08:00"), + Id: new(uint64(1234567890123456789)), + Kind: new("compute#globalAddress"), + SelfLink: new(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/addresses/%s", projectID, addressName)), } // Create a second global address for list testing addressName2 := "test-global-address-2" globalAddress2 := &computepb.Address{ Name: &addressName2, - Description: stringPtr("Second test global address"), - Address: stringPtr("203.0.113.13"), - AddressType: stringPtr("EXTERNAL"), - Status: stringPtr("RESERVED"), - Network: stringPtr("global/networks/test-network-2"), + Description: new("Second test global address"), + Address: new("203.0.113.13"), + AddressType: new("EXTERNAL"), + Status: new("RESERVED"), + Network: new("global/networks/test-network-2"), Labels: map[string]string{ "env": "prod", "team": "networking", }, - Region: stringPtr("global"), - NetworkTier: stringPtr("PREMIUM"), - CreationTimestamp: stringPtr("2023-01-16T11:45:00.000-08:00"), - Id: uint64Ptr(1234567890123456790), - Kind: stringPtr("compute#globalAddress"), - SelfLink: stringPtr(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/addresses/%s", projectID, addressName2)), + Region: new("global"), + NetworkTier: new("PREMIUM"), + CreationTimestamp: new("2023-01-16T11:45:00.000-08:00"), + Id: new(uint64(1234567890123456790)), + Kind: new("compute#globalAddress"), + SelfLink: new(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/addresses/%s", projectID, addressName2)), } globalAddresses := &computepb.AddressList{ @@ -193,16 +193,3 @@ func TestComputeGlobalAddress(t *testing.T) { } }) } - -// Helper functions for pointer creation -func stringPtr(s string) *string { - return &s -} - -func uint64Ptr(u uint64) *uint64 { - return &u -} - -func boolPtr(b bool) *bool { - return &b -} 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 95cb69dd..d43f66bd 100644 --- a/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go +++ b/sources/gcp/dynamic/adapters/compute-global-forwarding-rule_test.go @@ -24,74 +24,74 @@ func TestComputeGlobalForwardingRule(t *testing.T) { // Mock response for a global forwarding rule using protobuf types globalForwardingRule := &computepb.ForwardingRule{ - Id: uint64Ptr(1234567890123456789), - CreationTimestamp: stringPtr("2023-01-01T00:00:00.000-08:00"), - Name: stringPtr(forwardingRuleName), - Description: stringPtr("Test global forwarding rule"), - Region: stringPtr(""), - IPAddress: stringPtr("203.0.113.1"), - IPProtocol: stringPtr("TCP"), - PortRange: stringPtr("80"), - Target: stringPtr(fmt.Sprintf("projects/%s/global/targetHttpProxies/test-target-proxy", projectID)), - SelfLink: stringPtr(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/forwardingRules/%s", projectID, forwardingRuleName)), - LoadBalancingScheme: stringPtr("EXTERNAL"), - Subnetwork: stringPtr(fmt.Sprintf("projects/%s/regions/us-central1/subnetworks/test-subnet", projectID)), - Network: stringPtr(fmt.Sprintf("projects/%s/global/networks/default", projectID)), - BackendService: stringPtr(fmt.Sprintf("projects/%s/global/backendServices/test-backend-service", projectID)), - ServiceLabel: stringPtr("test-service"), - ServiceName: stringPtr(fmt.Sprintf("%s-test-service.c.%s.internal", forwardingRuleName, projectID)), - Kind: stringPtr("compute#forwardingRule"), - LabelFingerprint: stringPtr("42WmSpB8rSM="), + Id: new(uint64(1234567890123456789)), + CreationTimestamp: new("2023-01-01T00:00:00.000-08:00"), + Name: new(forwardingRuleName), + Description: new("Test global forwarding rule"), + Region: new(""), + IPAddress: new("203.0.113.1"), + IPProtocol: new("TCP"), + PortRange: new("80"), + Target: new(fmt.Sprintf("projects/%s/global/targetHttpProxies/test-target-proxy", projectID)), + SelfLink: new(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/forwardingRules/%s", projectID, forwardingRuleName)), + LoadBalancingScheme: new("EXTERNAL"), + Subnetwork: new(fmt.Sprintf("projects/%s/regions/us-central1/subnetworks/test-subnet", projectID)), + Network: new(fmt.Sprintf("projects/%s/global/networks/default", projectID)), + BackendService: new(fmt.Sprintf("projects/%s/global/backendServices/test-backend-service", projectID)), + ServiceLabel: new("test-service"), + ServiceName: new(fmt.Sprintf("%s-test-service.c.%s.internal", forwardingRuleName, projectID)), + Kind: new("compute#forwardingRule"), + LabelFingerprint: new("42WmSpB8rSM="), Labels: map[string]string{ "env": "test", "team": "devops", }, - NetworkTier: stringPtr("PREMIUM"), - AllowGlobalAccess: boolPtr(false), - AllowPscGlobalAccess: boolPtr(false), + NetworkTier: new("PREMIUM"), + AllowGlobalAccess: new(false), + AllowPscGlobalAccess: new(false), PscConnectionId: nil, - PscConnectionStatus: stringPtr("ACCEPTED"), - Fingerprint: stringPtr("abcd1234efgh5678"), + PscConnectionStatus: new("ACCEPTED"), + Fingerprint: new("abcd1234efgh5678"), } // Mock response for a second global forwarding rule using protobuf types globalForwardingRule2 := &computepb.ForwardingRule{ - Id: uint64Ptr(9876543210987654321), - CreationTimestamp: stringPtr("2023-01-02T00:00:00.000-08:00"), - Name: stringPtr("test-global-forwarding-rule-2"), - Description: stringPtr("Second test global forwarding rule"), - Region: stringPtr(""), - IPAddress: stringPtr("203.0.113.2"), - IPProtocol: stringPtr("TCP"), - PortRange: stringPtr("443"), - Target: stringPtr(fmt.Sprintf("projects/%s/global/targetHttpsProxies/test-target-proxy-2", projectID)), - SelfLink: stringPtr(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/forwardingRules/test-global-forwarding-rule-2", projectID)), - LoadBalancingScheme: stringPtr("EXTERNAL"), - Subnetwork: stringPtr(fmt.Sprintf("projects/%s/regions/us-west1/subnetworks/test-subnet-2", projectID)), - Network: stringPtr(fmt.Sprintf("projects/%s/global/networks/custom-network", projectID)), - BackendService: stringPtr(fmt.Sprintf("projects/%s/global/backendServices/test-backend-service-2", projectID)), - ServiceLabel: stringPtr("test-service-2"), - ServiceName: stringPtr("test-global-forwarding-rule-2-test-service-2.c." + projectID + ".internal"), - Kind: stringPtr("compute#forwardingRule"), - LabelFingerprint: stringPtr("xyz789abc123def="), + Id: new(uint64(9876543210987654321)), + CreationTimestamp: new("2023-01-02T00:00:00.000-08:00"), + Name: new("test-global-forwarding-rule-2"), + Description: new("Second test global forwarding rule"), + Region: new(""), + IPAddress: new("203.0.113.2"), + IPProtocol: new("TCP"), + PortRange: new("443"), + Target: new(fmt.Sprintf("projects/%s/global/targetHttpsProxies/test-target-proxy-2", projectID)), + SelfLink: new(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/forwardingRules/test-global-forwarding-rule-2", projectID)), + LoadBalancingScheme: new("EXTERNAL"), + Subnetwork: new(fmt.Sprintf("projects/%s/regions/us-west1/subnetworks/test-subnet-2", projectID)), + Network: new(fmt.Sprintf("projects/%s/global/networks/custom-network", projectID)), + BackendService: new(fmt.Sprintf("projects/%s/global/backendServices/test-backend-service-2", projectID)), + ServiceLabel: new("test-service-2"), + ServiceName: new("test-global-forwarding-rule-2-test-service-2.c." + projectID + ".internal"), + Kind: new("compute#forwardingRule"), + LabelFingerprint: new("xyz789abc123def="), Labels: map[string]string{ "env": "prod", "service": "web", }, - NetworkTier: stringPtr("PREMIUM"), - AllowGlobalAccess: boolPtr(true), - AllowPscGlobalAccess: boolPtr(true), - PscConnectionId: uint64Ptr(123), - PscConnectionStatus: stringPtr("ACCEPTED"), - Fingerprint: stringPtr("xyz789abc123def456"), + NetworkTier: new("PREMIUM"), + AllowGlobalAccess: new(true), + AllowPscGlobalAccess: new(true), + PscConnectionId: new(uint64(123)), + PscConnectionStatus: new("ACCEPTED"), + Fingerprint: new("xyz789abc123def456"), } // Mock response for list operation using protobuf types globalForwardingRulesList := &computepb.ForwardingRuleList{ - Kind: stringPtr("compute#forwardingRuleList"), - Id: stringPtr("projects/" + projectID + "/global/forwardingRules"), + Kind: new("compute#forwardingRuleList"), + Id: new("projects/" + projectID + "/global/forwardingRules"), Items: []*computepb.ForwardingRule{globalForwardingRule, globalForwardingRule2}, - SelfLink: stringPtr(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/forwardingRules", projectID)), + SelfLink: new(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/forwardingRules", projectID)), } sdpItemType := gcpshared.ComputeGlobalForwardingRule @@ -201,7 +201,7 @@ func TestComputeGlobalForwardingRule(t *testing.T) { // Test labels - check if labels exist before testing labels, err := sdpItem.GetAttributes().Get("labels") if err == nil { - labelsMap, ok := labels.(map[string]interface{}) + labelsMap, ok := labels.(map[string]any) if !ok { t.Fatalf("Expected labels to be a map[string]interface{}, got %T", labels) } @@ -295,7 +295,7 @@ func TestComputeGlobalForwardingRule(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/forwardingRules/%s", projectID, forwardingRuleName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Not found"}, + Body: map[string]any{"error": "Not found"}, }, } @@ -314,8 +314,8 @@ func TestComputeGlobalForwardingRule(t *testing.T) { t.Run("EmptyList", func(t *testing.T) { // Test with empty list response using protobuf types emptyListResponse := &computepb.ForwardingRuleList{ - Kind: stringPtr("compute#forwardingRuleList"), - Id: stringPtr("projects/" + projectID + "/global/forwardingRules"), + Kind: new("compute#forwardingRuleList"), + Id: new("projects/" + projectID + "/global/forwardingRules"), Items: []*computepb.ForwardingRule{}, } 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 a3134320..fca9cb2c 100644 --- a/sources/gcp/dynamic/adapters/compute-http-health-check_test.go +++ b/sources/gcp/dynamic/adapters/compute-http-health-check_test.go @@ -22,18 +22,18 @@ func TestComputeHttpHealthCheck(t *testing.T) { healthCheckName := "test-health-check" // Use map since HTTPHealthCheck protobuf doesn't have Name field - healthCheck := map[string]interface{}{ + healthCheck := map[string]any{ "name": healthCheckName, "host": "example.com", } healthCheckName2 := "test-health-check-2" - healthCheck2 := map[string]interface{}{ + healthCheck2 := map[string]any{ "name": healthCheckName2, } - healthCheckList := map[string]interface{}{ - "items": []interface{}{healthCheck, healthCheck2}, + healthCheckList := map[string]any{ + "items": []any{healthCheck, healthCheck2}, } sdpItemType := gcpshared.ComputeHttpHealthCheck @@ -91,7 +91,7 @@ func TestComputeHttpHealthCheck(t *testing.T) { // 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{}{ + healthCheckWithIP := map[string]any{ "name": "test-health-check-ip", "host": "192.168.1.1", } @@ -192,7 +192,7 @@ func TestComputeHttpHealthCheck(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/httpHealthChecks/%s", projectID, healthCheckName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Health check not found"}, + Body: map[string]any{"error": "Health check not found"}, }, } 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 a4e723ff..f6ee74ed 100644 --- a/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go +++ b/sources/gcp/dynamic/adapters/compute-network-endpoint-group_test.go @@ -152,7 +152,7 @@ func TestComputeNetworkEndpointGroup(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/zones/%s/networkEndpointGroups/%s", projectID, zone, negName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "NEG not found"}, + Body: map[string]any{"error": "NEG not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-network_test.go b/sources/gcp/dynamic/adapters/compute-network_test.go index 4968ad42..94f6bbf2 100644 --- a/sources/gcp/dynamic/adapters/compute-network_test.go +++ b/sources/gcp/dynamic/adapters/compute-network_test.go @@ -129,7 +129,7 @@ func TestComputeNetwork(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/networks/%s", projectID, networkName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Network not found"}, + Body: map[string]any{"error": "Network not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-project_test.go b/sources/gcp/dynamic/adapters/compute-project_test.go index d475df6d..68ef96d2 100644 --- a/sources/gcp/dynamic/adapters/compute-project_test.go +++ b/sources/gcp/dynamic/adapters/compute-project_test.go @@ -79,7 +79,7 @@ func TestComputeProject(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s?fields=name", projectID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Project not found"}, + Body: map[string]any{"error": "Project not found"}, }, } 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 623ec276..a924dbd5 100644 --- a/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go +++ b/sources/gcp/dynamic/adapters/compute-public-delegated-prefix_test.go @@ -187,7 +187,7 @@ func TestComputePublicDelegatedPrefix(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/regions/%s/publicDelegatedPrefixes/%s", projectID, region, prefixName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Prefix not found"}, + Body: map[string]any{"error": "Prefix not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-region-commitment_test.go b/sources/gcp/dynamic/adapters/compute-region-commitment_test.go index a56ef8e8..59262975 100644 --- a/sources/gcp/dynamic/adapters/compute-region-commitment_test.go +++ b/sources/gcp/dynamic/adapters/compute-region-commitment_test.go @@ -128,7 +128,7 @@ func TestComputeRegionCommitment(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/regions/%s/commitments/%s", projectID, region, commitmentName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Commitment not found"}, + Body: map[string]any{"error": "Commitment not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-route_test.go b/sources/gcp/dynamic/adapters/compute-route_test.go index 79a90518..cd9d038d 100644 --- a/sources/gcp/dynamic/adapters/compute-route_test.go +++ b/sources/gcp/dynamic/adapters/compute-route_test.go @@ -141,7 +141,7 @@ func TestComputeRoute(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/routes/%s", projectID, routeName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Route not found"}, + Body: map[string]any{"error": "Route not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-router_test.go b/sources/gcp/dynamic/adapters/compute-router_test.go index 1171e13d..5bb80aba 100644 --- a/sources/gcp/dynamic/adapters/compute-router_test.go +++ b/sources/gcp/dynamic/adapters/compute-router_test.go @@ -27,41 +27,41 @@ func TestComputeRouter(t *testing.T) { // Create mock protobuf object router := &computepb.Router{ - Name: stringPtr(routerName), - Description: stringPtr("Test Router"), - Network: stringPtr(fmt.Sprintf("projects/%s/global/networks/default", projectID)), - Region: stringPtr(fmt.Sprintf("projects/%s/regions/%s", projectID, region)), + Name: new(routerName), + Description: new("Test Router"), + Network: new(fmt.Sprintf("projects/%s/global/networks/default", projectID)), + Region: new(fmt.Sprintf("projects/%s/regions/%s", projectID, region)), Interfaces: []*computepb.RouterInterface{ { - Name: stringPtr("interface-1"), - LinkedInterconnectAttachment: stringPtr(fmt.Sprintf("projects/%s/regions/%s/interconnectAttachments/test-attachment", projectID, region)), - PrivateIpAddress: stringPtr("10.0.0.1"), - Subnetwork: stringPtr(fmt.Sprintf("projects/%s/regions/%s/subnetworks/test-subnet", projectID, region)), - LinkedVpnTunnel: stringPtr(fmt.Sprintf("projects/%s/regions/%s/vpnTunnels/test-tunnel", projectID, region)), + Name: new("interface-1"), + LinkedInterconnectAttachment: new(fmt.Sprintf("projects/%s/regions/%s/interconnectAttachments/test-attachment", projectID, region)), + PrivateIpAddress: new("10.0.0.1"), + Subnetwork: new(fmt.Sprintf("projects/%s/regions/%s/subnetworks/test-subnet", projectID, region)), + LinkedVpnTunnel: new(fmt.Sprintf("projects/%s/regions/%s/vpnTunnels/test-tunnel", projectID, region)), }, }, BgpPeers: []*computepb.RouterBgpPeer{ { - Name: stringPtr("bgp-peer-1"), - PeerIpAddress: stringPtr("192.168.1.1"), - IpAddress: stringPtr("192.168.1.2"), - Ipv4NexthopAddress: stringPtr("192.168.1.3"), - PeerIpv4NexthopAddress: stringPtr("192.168.1.4"), + Name: new("bgp-peer-1"), + PeerIpAddress: new("192.168.1.1"), + IpAddress: new("192.168.1.2"), + Ipv4NexthopAddress: new("192.168.1.3"), + PeerIpv4NexthopAddress: new("192.168.1.4"), }, }, Nats: []*computepb.RouterNat{ { - Name: stringPtr("nat-1"), + Name: new("nat-1"), NatIps: []string{"203.0.113.1", "203.0.113.2"}, DrainNatIps: []string{"203.0.113.3"}, Subnetworks: []*computepb.RouterNatSubnetworkToNat{ { - Name: stringPtr(fmt.Sprintf("projects/%s/regions/%s/subnetworks/nat-subnet", projectID, region)), + Name: new(fmt.Sprintf("projects/%s/regions/%s/subnetworks/nat-subnet", projectID, region)), }, }, Nat64Subnetworks: []*computepb.RouterNatSubnetworkToNat64{ { - Name: stringPtr(fmt.Sprintf("projects/%s/regions/%s/subnetworks/nat64-subnet", projectID, region)), + Name: new(fmt.Sprintf("projects/%s/regions/%s/subnetworks/nat64-subnet", projectID, region)), }, }, }, @@ -71,10 +71,10 @@ func TestComputeRouter(t *testing.T) { // Create second router for list testing routerName2 := "test-router-2" router2 := &computepb.Router{ - Name: stringPtr(routerName2), - Description: stringPtr("Test Router 2"), - Network: stringPtr(fmt.Sprintf("projects/%s/global/networks/default", projectID)), - Region: stringPtr(fmt.Sprintf("projects/%s/regions/%s", projectID, region)), + Name: new(routerName2), + Description: new("Test Router 2"), + Network: new(fmt.Sprintf("projects/%s/global/networks/default", projectID)), + Region: new(fmt.Sprintf("projects/%s/regions/%s", projectID, region)), } // Create list response with multiple items @@ -305,7 +305,7 @@ func TestComputeRouter(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/regions/%s/routers/%s", projectID, region, routerName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Router not found"}, + Body: map[string]any{"error": "Router not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go b/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go index b73f49ca..826a5109 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-certificate_test.go @@ -23,20 +23,20 @@ func TestComputeSSLCertificate(t *testing.T) { // Create mock protobuf object certificate := &computepb.SslCertificate{ - Name: stringPtr(certificateName), - Description: stringPtr("Test SSL Certificate"), - Certificate: stringPtr("-----BEGIN CERTIFICATE-----\nMIIC...test certificate data...\n-----END CERTIFICATE-----"), - PrivateKey: stringPtr("-----BEGIN PRIVATE KEY-----\nMIIE...test private key data...\n-----END PRIVATE KEY-----"), - SelfLink: stringPtr(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/sslCertificates/%s", projectID, certificateName)), + Name: new(certificateName), + Description: new("Test SSL Certificate"), + Certificate: new("-----BEGIN CERTIFICATE-----\nMIIC...test certificate data...\n-----END CERTIFICATE-----"), + PrivateKey: new("-----BEGIN PRIVATE KEY-----\nMIIE...test private key data...\n-----END PRIVATE KEY-----"), + SelfLink: new(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/sslCertificates/%s", projectID, certificateName)), } // Create second certificate for list testing certificateName2 := "test-ssl-certificate-2" certificate2 := &computepb.SslCertificate{ - Name: stringPtr(certificateName2), - Description: stringPtr("Test SSL Certificate 2"), - Certificate: stringPtr("-----BEGIN CERTIFICATE-----\nMIIC...test certificate data 2...\n-----END CERTIFICATE-----"), - SelfLink: stringPtr(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/sslCertificates/%s", projectID, certificateName2)), + Name: new(certificateName2), + Description: new("Test SSL Certificate 2"), + Certificate: new("-----BEGIN CERTIFICATE-----\nMIIC...test certificate data 2...\n-----END CERTIFICATE-----"), + SelfLink: new(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/sslCertificates/%s", projectID, certificateName2)), } // Create list response with multiple items @@ -133,7 +133,7 @@ func TestComputeSSLCertificate(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/sslCertificates/%s", projectID, certificateName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "SSL Certificate not found"}, + Body: map[string]any{"error": "SSL Certificate not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go b/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go index e0ee4f86..f1bb0295 100644 --- a/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go +++ b/sources/gcp/dynamic/adapters/compute-ssl-policy_test.go @@ -99,7 +99,7 @@ func TestComputeSSLPolicy(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/sslPolicies/%s", projectID, policyName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "SSL policy not found"}, + Body: map[string]any{"error": "SSL policy not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-subnetwork_test.go b/sources/gcp/dynamic/adapters/compute-subnetwork_test.go index 84643afc..b15ebcfd 100644 --- a/sources/gcp/dynamic/adapters/compute-subnetwork_test.go +++ b/sources/gcp/dynamic/adapters/compute-subnetwork_test.go @@ -117,7 +117,7 @@ func TestComputeSubnetwork(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/regions/%s/subnetworks/%s", projectID, region, subnetworkName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Subnetwork not found"}, + Body: map[string]any{"error": "Subnetwork not found"}, }, } 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 2f2be523..fe682162 100644 --- a/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-http-proxy_test.go @@ -112,7 +112,7 @@ func TestComputeTargetHttpProxy(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/targetHttpProxies/%s", projectID, proxyName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Proxy not found"}, + Body: map[string]any{"error": "Proxy not found"}, }, } 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 6354aa99..345196d1 100644 --- a/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-https-proxy_test.go @@ -128,7 +128,7 @@ func TestComputeTargetHttpsProxy(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/targetHttpsProxies/%s", projectID, proxyName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Proxy not found"}, + Body: map[string]any{"error": "Proxy not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-target-pool_test.go b/sources/gcp/dynamic/adapters/compute-target-pool_test.go index aa5fbb2e..922f081f 100644 --- a/sources/gcp/dynamic/adapters/compute-target-pool_test.go +++ b/sources/gcp/dynamic/adapters/compute-target-pool_test.go @@ -192,7 +192,7 @@ func TestComputeTargetPool(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/regions/%s/targetPools/%s", projectID, region, poolName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Target pool not found"}, + Body: map[string]any{"error": "Target pool not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-url-map_test.go b/sources/gcp/dynamic/adapters/compute-url-map_test.go index c997f8bb..634bac8d 100644 --- a/sources/gcp/dynamic/adapters/compute-url-map_test.go +++ b/sources/gcp/dynamic/adapters/compute-url-map_test.go @@ -268,7 +268,7 @@ func TestComputeUrlMap(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/urlMaps/%s", projectID, urlMapName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "URL map not found"}, + Body: map[string]any{"error": "URL map not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go b/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go index 984becbc..5f1f3ecd 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-gateway_test.go @@ -135,7 +135,7 @@ func TestComputeVpnGateway(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/regions/%s/vpnGateways/%s", projectID, region, gatewayName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "VPN gateway not found"}, + Body: map[string]any{"error": "VPN gateway not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go b/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go index 4ea038f1..a17a0f29 100644 --- a/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go +++ b/sources/gcp/dynamic/adapters/compute-vpn-tunnel_test.go @@ -161,7 +161,7 @@ func TestComputeVpnTunnel(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/regions/%s/vpnTunnels/%s", projectID, region, tunnelName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "VPN tunnel not found"}, + Body: map[string]any{"error": "VPN tunnel not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/container-cluster_test.go b/sources/gcp/dynamic/adapters/container-cluster_test.go index 649f5214..5c4f967b 100644 --- a/sources/gcp/dynamic/adapters/container-cluster_test.go +++ b/sources/gcp/dynamic/adapters/container-cluster_test.go @@ -315,7 +315,7 @@ func TestContainerCluster(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://container.googleapis.com/v1/projects/%s/locations/%s/clusters/%s", projectID, location, clusterName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Cluster not found"}, + Body: map[string]any{"error": "Cluster not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/container-node-pool_test.go b/sources/gcp/dynamic/adapters/container-node-pool_test.go index 0b01b9c3..51b6c0a6 100644 --- a/sources/gcp/dynamic/adapters/container-node-pool_test.go +++ b/sources/gcp/dynamic/adapters/container-node-pool_test.go @@ -210,7 +210,7 @@ func TestContainerNodePool(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://container.googleapis.com/v1/projects/%s/locations/%s/clusters/%s/nodePools/%s", projectID, location, clusterName, nodePoolName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Node pool not found"}, + Body: map[string]any{"error": "Node pool not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/dataform-repository_test.go b/sources/gcp/dynamic/adapters/dataform-repository_test.go index 70518f10..9b553c2c 100644 --- a/sources/gcp/dynamic/adapters/dataform-repository_test.go +++ b/sources/gcp/dynamic/adapters/dataform-repository_test.go @@ -146,7 +146,7 @@ func TestDataformRepository(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://dataform.googleapis.com/v1/projects/%s/locations/%s/repositories/%s", projectID, location, repositoryName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Repository not found"}, + Body: map[string]any{"error": "Repository not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go b/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go index fcafe96b..1b76d988 100644 --- a/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-aspect-type_test.go @@ -58,8 +58,8 @@ func TestDataplexAspectType(t *testing.T) { // Create the list response using a map structure instead of the protobuf ListAspectTypesResponse // This is necessary because the dynamic adapter expects JSON-serializable structures // Individual items use proper SDK types, but the list wrapper uses a simple map - aspectTypesList := map[string]interface{}{ - "aspectTypes": []interface{}{aspectType, aspectType2}, + aspectTypesList := map[string]any{ + "aspectTypes": []any{aspectType, aspectType2}, } sdpItemType := gcpshared.DataplexAspectType @@ -236,7 +236,7 @@ func TestDataplexAspectType(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://dataplex.googleapis.com/v1/projects/%s/locations/%s/aspectTypes/nonexistent", projectID, location): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": map[string]interface{}{"code": 404, "message": "AspectType not found"}}, + Body: map[string]any{"error": map[string]any{"code": 404, "message": "AspectType not found"}}, }, } diff --git a/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go b/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go index 8f67d5c2..e0e9f057 100644 --- a/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-data-scan_test.go @@ -152,7 +152,7 @@ func TestDataplexDataScan(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://dataplex.googleapis.com/v1/projects/%s/locations/%s/dataScans/%s", projectID, location, dataScanName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Data scan not found"}, + Body: map[string]any{"error": "Data scan not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go b/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go index a2b71164..081f35dc 100644 --- a/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go +++ b/sources/gcp/dynamic/adapters/dataplex-entry-group_test.go @@ -122,7 +122,7 @@ func TestDataplexEntryGroup(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://dataplex.googleapis.com/v1/projects/%s/locations/%s/entryGroups/%s", projectID, location, entryGroupID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Entry group not found"}, + Body: map[string]any{"error": "Entry group not found"}, }, } 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 1b8c6348..9761b985 100644 --- a/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go +++ b/sources/gcp/dynamic/adapters/dataproc-auto-scaling-policy_test.go @@ -149,7 +149,7 @@ func TestDataprocAutoscalingPolicy(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://dataproc.googleapis.com/v1/projects/%s/regions/%s/autoscalingPolicies/%s", projectID, region, policyName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Policy not found"}, + Body: map[string]any{"error": "Policy not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/dataproc-cluster_test.go b/sources/gcp/dynamic/adapters/dataproc-cluster_test.go index a9daf0c6..978d14c2 100644 --- a/sources/gcp/dynamic/adapters/dataproc-cluster_test.go +++ b/sources/gcp/dynamic/adapters/dataproc-cluster_test.go @@ -202,7 +202,7 @@ func TestDataprocCluster(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://dataproc.googleapis.com/v1/projects/%s/regions/%s/clusters/%s", projectID, region, clusterName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Cluster not found"}, + Body: map[string]any{"error": "Cluster not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/dns-managed-zone_test.go b/sources/gcp/dynamic/adapters/dns-managed-zone_test.go index 59bf5b46..90afa549 100644 --- a/sources/gcp/dynamic/adapters/dns-managed-zone_test.go +++ b/sources/gcp/dynamic/adapters/dns-managed-zone_test.go @@ -156,7 +156,7 @@ func TestDNSManagedZone(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://dns.googleapis.com/dns/v1/projects/%s/managedZones/%s", projectID, zoneName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Managed zone not found"}, + Body: map[string]any{"error": "Managed zone not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go b/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go index 1dd41c5c..adfc26c4 100644 --- a/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go +++ b/sources/gcp/dynamic/adapters/essential-contacts-contact_test.go @@ -121,7 +121,7 @@ func TestEssentialContactsContact(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://essentialcontacts.googleapis.com/v1/projects/%s/contacts/%s", projectID, contactID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Contact not found"}, + Body: map[string]any{"error": "Contact not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/file-instance_test.go b/sources/gcp/dynamic/adapters/file-instance_test.go index d09cb5cc..190c03a2 100644 --- a/sources/gcp/dynamic/adapters/file-instance_test.go +++ b/sources/gcp/dynamic/adapters/file-instance_test.go @@ -206,7 +206,7 @@ func TestFileInstance(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://file.googleapis.com/v1/projects/%s/locations/%s/instances/%s", projectID, location, instanceName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Instance not found"}, + Body: map[string]any{"error": "Instance not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/iam-role_test.go b/sources/gcp/dynamic/adapters/iam-role_test.go index 058cba6c..c3b09a6b 100644 --- a/sources/gcp/dynamic/adapters/iam-role_test.go +++ b/sources/gcp/dynamic/adapters/iam-role_test.go @@ -86,7 +86,7 @@ func TestIAMRole(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://iam.googleapis.com/v1/projects/%s/roles/%s", projectID, roleName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Role not found"}, + Body: map[string]any{"error": "Role not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/logging-bucket_test.go b/sources/gcp/dynamic/adapters/logging-bucket_test.go index 563cfa66..8621cf59 100644 --- a/sources/gcp/dynamic/adapters/logging-bucket_test.go +++ b/sources/gcp/dynamic/adapters/logging-bucket_test.go @@ -121,7 +121,7 @@ func TestLoggingBucket(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://logging.googleapis.com/v2/projects/%s/locations/%s/buckets/%s", projectID, location, bucketName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Bucket not found"}, + Body: map[string]any{"error": "Bucket not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/logging-link_test.go b/sources/gcp/dynamic/adapters/logging-link_test.go index 3b333b2f..b8dd40fc 100644 --- a/sources/gcp/dynamic/adapters/logging-link_test.go +++ b/sources/gcp/dynamic/adapters/logging-link_test.go @@ -114,7 +114,7 @@ func TestLoggingLink(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://logging.googleapis.com/v2/projects/%s/locations/%s/buckets/%s/links/%s", projectID, location, bucketName, linkName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Link not found"}, + Body: map[string]any{"error": "Link not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/logging-saved-query_test.go b/sources/gcp/dynamic/adapters/logging-saved-query_test.go index 9fd02880..22c873e9 100644 --- a/sources/gcp/dynamic/adapters/logging-saved-query_test.go +++ b/sources/gcp/dynamic/adapters/logging-saved-query_test.go @@ -88,7 +88,7 @@ func TestLoggingSavedQuery(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://logging.googleapis.com/v2/projects/%s/locations/%s/savedQueries/%s", projectID, location, queryName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Saved query not found"}, + Body: map[string]any{"error": "Saved query not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go b/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go index 7c10d7a0..da8b4696 100644 --- a/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-alert-policy_test.go @@ -198,7 +198,7 @@ func TestMonitoringAlertPolicy(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://monitoring.googleapis.com/v3/projects/%s/alertPolicies/%s", projectID, policyID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Alert policy not found"}, + Body: map[string]any{"error": "Alert policy not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go index 2ed436e8..f6126390 100644 --- a/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-custom-dashboard_test.go @@ -121,7 +121,7 @@ func TestMonitoringCustomDashboard(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://monitoring.googleapis.com/v1/projects/%s/dashboards/%s", projectID, dashboardID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Dashboard not found"}, + Body: map[string]any{"error": "Dashboard not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go b/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go index 3057412b..26cce5b3 100644 --- a/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go +++ b/sources/gcp/dynamic/adapters/monitoring-notification-channel_test.go @@ -137,7 +137,7 @@ func TestMonitoringNotificationChannel(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://monitoring.googleapis.com/v3/projects/%s/notificationChannels/%s", projectID, channelID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Notification channel not found"}, + Body: map[string]any{"error": "Notification channel not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go b/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go index a476e090..ced3b00f 100644 --- a/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go +++ b/sources/gcp/dynamic/adapters/orgpolicy-policy_test.go @@ -165,7 +165,7 @@ func TestOrgPolicyPolicy(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://orgpolicy.googleapis.com/v2/projects/%s/policies/%s", projectID, policyName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Policy not found"}, + Body: map[string]any{"error": "Policy not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/pubsub-subscription_test.go b/sources/gcp/dynamic/adapters/pubsub-subscription_test.go index d1378ca2..a5a5a3bc 100644 --- a/sources/gcp/dynamic/adapters/pubsub-subscription_test.go +++ b/sources/gcp/dynamic/adapters/pubsub-subscription_test.go @@ -171,7 +171,7 @@ func TestPubSubSubscription(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://pubsub.googleapis.com/v1/projects/%s/subscriptions/%s", projectID, subscriptionName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Subscription not found"}, + Body: map[string]any{"error": "Subscription not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/pubsub-topic_test.go b/sources/gcp/dynamic/adapters/pubsub-topic_test.go index a88b0302..0265b084 100644 --- a/sources/gcp/dynamic/adapters/pubsub-topic_test.go +++ b/sources/gcp/dynamic/adapters/pubsub-topic_test.go @@ -115,7 +115,7 @@ func TestPubSubTopic(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://pubsub.googleapis.com/v1/projects/%s/topics/%s", projectID, topicName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Topic not found"}, + Body: map[string]any{"error": "Topic not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/redis-instance_test.go b/sources/gcp/dynamic/adapters/redis-instance_test.go index 19066bea..dc2a9309 100644 --- a/sources/gcp/dynamic/adapters/redis-instance_test.go +++ b/sources/gcp/dynamic/adapters/redis-instance_test.go @@ -223,7 +223,7 @@ func TestRedisInstance(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://redis.googleapis.com/v1/projects/%s/locations/%s/instances/%s", projectID, location, instanceName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Redis instance not found"}, + Body: map[string]any{"error": "Redis instance not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/run-revision_test.go b/sources/gcp/dynamic/adapters/run-revision_test.go index b5a813b5..c1525c37 100644 --- a/sources/gcp/dynamic/adapters/run-revision_test.go +++ b/sources/gcp/dynamic/adapters/run-revision_test.go @@ -113,7 +113,7 @@ func TestRunRevision(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://run.googleapis.com/v2/projects/%s/locations/%s/services/%s/revisions/%s", projectID, location, serviceName, revisionName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Revision not found"}, + Body: map[string]any{"error": "Revision not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/run-service_test.go b/sources/gcp/dynamic/adapters/run-service_test.go index 5d57ff4b..382801c0 100644 --- a/sources/gcp/dynamic/adapters/run-service_test.go +++ b/sources/gcp/dynamic/adapters/run-service_test.go @@ -313,7 +313,7 @@ func TestRunService(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://run.googleapis.com/v2/projects/%s/locations/%s/services/%s", projectID, location, serviceName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Service not found"}, + Body: map[string]any{"error": "Service not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/secret-manager-secret_test.go b/sources/gcp/dynamic/adapters/secret-manager-secret_test.go index 4a6f9924..84030fc0 100644 --- a/sources/gcp/dynamic/adapters/secret-manager-secret_test.go +++ b/sources/gcp/dynamic/adapters/secret-manager-secret_test.go @@ -220,7 +220,7 @@ func TestSecretManagerSecret(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://secretmanager.googleapis.com/v1/projects/%s/secrets/%s", projectID, secretID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Secret not found"}, + Body: map[string]any{"error": "Secret not found"}, }, } 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 442c70d9..872d08e9 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 @@ -116,7 +116,7 @@ func TestSecurityCenterManagementSecurityCenterService(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://securitycentermanagement.googleapis.com/v1/projects/%s/locations/%s/securityCenterServices/%s", projectID, location, serviceName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Service not found"}, + Body: map[string]any{"error": "Service not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go b/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go index 96ba6461..ca261d0e 100644 --- a/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go +++ b/sources/gcp/dynamic/adapters/service-directory-endpoint_test.go @@ -157,7 +157,7 @@ func TestServiceDirectoryEndpoint(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://servicedirectory.googleapis.com/v1/projects/%s/locations/%s/namespaces/%s/services/%s/endpoints/%s", projectID, location, namespace, serviceName, endpointName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Endpoint not found"}, + Body: map[string]any{"error": "Endpoint not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/service-usage-service_test.go b/sources/gcp/dynamic/adapters/service-usage-service_test.go index 51ced862..2fc8fab5 100644 --- a/sources/gcp/dynamic/adapters/service-usage-service_test.go +++ b/sources/gcp/dynamic/adapters/service-usage-service_test.go @@ -105,7 +105,7 @@ func TestServiceUsageService(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://serviceusage.googleapis.com/v1/projects/%s/services/%s", projectID, serviceName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Service not found"}, + Body: map[string]any{"error": "Service not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/sql-admin-backup_test.go b/sources/gcp/dynamic/adapters/sql-admin-backup_test.go index 1ff3843b..30341e55 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-backup_test.go +++ b/sources/gcp/dynamic/adapters/sql-admin-backup_test.go @@ -160,7 +160,7 @@ func TestSQLAdminBackup(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://sqladmin.googleapis.com/v1/projects/%s/backups/%s", projectID, backupName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Backup not found"}, + Body: map[string]any{"error": "Backup not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/sql-admin-instance_test.go b/sources/gcp/dynamic/adapters/sql-admin-instance_test.go index 3491a795..554159be 100644 --- a/sources/gcp/dynamic/adapters/sql-admin-instance_test.go +++ b/sources/gcp/dynamic/adapters/sql-admin-instance_test.go @@ -241,7 +241,7 @@ func TestSQLAdminInstance(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://sqladmin.googleapis.com/sql/v1/projects/%s/instances/%s", projectID, instanceName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Instance not found"}, + Body: map[string]any{"error": "Instance not found"}, }, } diff --git a/sources/gcp/dynamic/adapters/storage-bucket_test.go b/sources/gcp/dynamic/adapters/storage-bucket_test.go index 937922be..5a6bbb46 100644 --- a/sources/gcp/dynamic/adapters/storage-bucket_test.go +++ b/sources/gcp/dynamic/adapters/storage-bucket_test.go @@ -110,7 +110,7 @@ func TestStorageBucket(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s", bucketName): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Bucket not found"}, + Body: map[string]any{"error": "Bucket not found"}, }, } 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 b8f85d5c..05585a34 100644 --- a/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go +++ b/sources/gcp/dynamic/adapters/storage-transfer-transfer-job_test.go @@ -234,7 +234,7 @@ func TestStorageTransferTransferJob(t *testing.T) { errorResponses := map[string]shared.MockResponse{ fmt.Sprintf("https://storagetransfer.googleapis.com/v1/transferJobs/%s?projectId=%s", jobID, projectID): { StatusCode: http.StatusNotFound, - Body: map[string]interface{}{"error": "Transfer job not found"}, + Body: map[string]any{"error": "Transfer job not found"}, }, } diff --git a/sources/gcp/dynamic/shared.go b/sources/gcp/dynamic/shared.go index 82d24eca..d058b5e9 100644 --- a/sources/gcp/dynamic/shared.go +++ b/sources/gcp/dynamic/shared.go @@ -17,8 +17,8 @@ import ( "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" + gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/shared" ) @@ -96,7 +96,7 @@ func externalToSDP( ctx context.Context, location gcpshared.LocationInfo, uniqueAttrKeys []string, - resp map[string]interface{}, + resp map[string]any, sdpAssetType shared.ItemType, linker *gcpshared.Linker, nameSelector string, @@ -148,13 +148,13 @@ func externalToSDP( return sdpItem, nil } -func externalCallSingle(ctx context.Context, httpCli *http.Client, url string) (map[string]interface{}, error) { +func externalCallSingle(ctx context.Context, httpCli *http.Client, url string) (map[string]any, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } - resp, err := httpCli.Do(req) + resp, err := httpCli.Do(req) //nolint:gosec // G107 (SSRF): URL built from GCP API discovery document endpoints and project config, not user input if err != nil { return nil, err } @@ -194,7 +194,7 @@ func externalCallSingle(ctx context.Context, httpCli *http.Client, url string) ( return nil, err } - var result map[string]interface{} + var result map[string]any if err = json.Unmarshal(data, &result); err != nil { return nil, err } @@ -215,7 +215,7 @@ func externalCallMulti(ctx context.Context, itemsSelector string, httpCli *http. return err } - resp, err := httpCli.Do(req) + resp, err := httpCli.Do(req) //nolint:gosec // G107 (SSRF): URL built from GCP API discovery document endpoints with pagination token from GCP responses if err != nil { return err } @@ -250,7 +250,7 @@ func externalCallMulti(ctx context.Context, itemsSelector string, httpCli *http. return err } - var result map[string]interface{} + var result map[string]any if err = json.Unmarshal(data, &result); err != nil { return err } @@ -282,7 +282,7 @@ func externalCallMulti(ctx context.Context, itemsSelector string, httpCli *http. // Add items from this page to our collection for _, item := range items { - if itemMap, ok := item.(map[string]interface{}); ok { + if itemMap, ok := item.(map[string]any); ok { // If out channel is provided, send the item to it select { case out <- itemMap: @@ -361,7 +361,7 @@ func aggregateSDPItems(ctx context.Context, a Adapter, url string, location gcps itemsSelector = a.listResponseSelector } - out := make(chan map[string]interface{}) + out := make(chan map[string]any) p := pool.New().WithErrors().WithContext(ctx) p.Go(func(ctx context.Context) error { defer close(out) @@ -412,7 +412,7 @@ func streamSDPItems(ctx context.Context, a Adapter, url string, location gcpshar itemsSelector = a.listResponseSelector } - out := make(chan map[string]interface{}) + out := make(chan map[string]any) p := pool.New().WithErrors().WithContext(ctx) p.Go(func(ctx context.Context) error { defer close(out) diff --git a/sources/gcp/dynamic/shared_test.go b/sources/gcp/dynamic/shared_test.go index 1c7e099d..c11528a7 100644 --- a/sources/gcp/dynamic/shared_test.go +++ b/sources/gcp/dynamic/shared_test.go @@ -22,7 +22,7 @@ func Test_externalToSDP(t *testing.T) { type args struct { location gcpshared.LocationInfo uniqueAttrKeys []string - resp map[string]interface{} + resp map[string]any sdpAssetType shared.ItemType nameSelector string } @@ -38,9 +38,9 @@ func Test_externalToSDP(t *testing.T) { args: args{ location: testLocation, uniqueAttrKeys: []string{"projects", "locations", "instances"}, - resp: map[string]interface{}{ + resp: map[string]any{ "name": "projects/test-project/locations/us-central1/instances/instance-1", - "labels": map[string]interface{}{"env": "prod"}, + "labels": map[string]any{"env": "prod"}, "foo": "bar", }, sdpAssetType: gcpshared.ComputeInstance, @@ -67,10 +67,10 @@ func Test_externalToSDP(t *testing.T) { args: args{ location: testLocation, uniqueAttrKeys: []string{"projects", "locations", "instances"}, - resp: map[string]interface{}{ + resp: map[string]any{ // There is name, but it does not include uniqueAttrKeys, expected to use the name as is. "name": "instance-1", - "labels": map[string]interface{}{"env": "prod"}, + "labels": map[string]any{"env": "prod"}, "foo": "bar", }, sdpAssetType: gcpshared.ComputeInstance, @@ -97,8 +97,8 @@ func Test_externalToSDP(t *testing.T) { args: args{ location: testLocation, uniqueAttrKeys: []string{"projects", "locations", "instances"}, - resp: map[string]interface{}{ - "labels": map[string]interface{}{"env": "prod"}, + resp: map[string]any{ + "labels": map[string]any{"env": "prod"}, "foo": "bar", }, sdpAssetType: gcpshared.ComputeInstance, @@ -111,9 +111,9 @@ func Test_externalToSDP(t *testing.T) { args: args{ location: testLocation, uniqueAttrKeys: []string{"projects", "locations", "instances"}, - resp: map[string]interface{}{ + resp: map[string]any{ "instanceName": "instance-1", - "labels": map[string]interface{}{"env": "prod"}, + "labels": map[string]any{"env": "prod"}, "foo": "bar", }, sdpAssetType: gcpshared.ComputeInstance, @@ -141,7 +141,7 @@ func Test_externalToSDP(t *testing.T) { args: args{ location: testLocation, uniqueAttrKeys: []string{"projects", "locations", "instances"}, - resp: map[string]interface{}{ + resp: map[string]any{ "name": "projects/test-project/locations/us-central1/instances/instance-2", "foo": "baz", }, diff --git a/sources/gcp/integration-tests/compute-address_test.go b/sources/gcp/integration-tests/compute-address_test.go index 008b1c83..332ad10d 100644 --- a/sources/gcp/integration-tests/compute-address_test.go +++ b/sources/gcp/integration-tests/compute-address_test.go @@ -12,7 +12,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -118,12 +117,12 @@ func TestComputeAddressIntegration(t *testing.T) { func createComputeAddress(ctx context.Context, client *compute.AddressesClient, projectID, region, addressName string) error { // Define the address configuration address := &computepb.Address{ - Name: ptr.To(addressName), + Name: new(addressName), Labels: map[string]string{ "test": "integration", }, - NetworkTier: ptr.To("PREMIUM"), - Region: ptr.To(region), + NetworkTier: new("PREMIUM"), + Region: new(region), } // Create the address diff --git a/sources/gcp/integration-tests/compute-autoscaler_test.go b/sources/gcp/integration-tests/compute-autoscaler_test.go index 8853286e..52ec7f75 100644 --- a/sources/gcp/integration-tests/compute-autoscaler_test.go +++ b/sources/gcp/integration-tests/compute-autoscaler_test.go @@ -12,7 +12,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -187,20 +186,20 @@ func TestComputeAutoscalerIntegration(t *testing.T) { func createComputeInstanceTemplate(ctx context.Context, client *compute.InstanceTemplatesClient, projectID, name string) error { // Create a new instance template instanceTemplate := &computepb.InstanceTemplate{ - Name: ptr.To(name), + Name: new(name), Properties: &computepb.InstanceProperties{ Disks: []*computepb.AttachedDisk{ { - AutoDelete: ptr.To(true), - Boot: ptr.To(true), - DeviceName: ptr.To(name), + AutoDelete: new(true), + Boot: new(true), + DeviceName: new(name), InitializeParams: &computepb.AttachedDiskInitializeParams{ - DiskSizeGb: ptr.To(int64(10)), - DiskType: ptr.To("pd-balanced"), - SourceImage: ptr.To("projects/debian-cloud/global/images/debian-12-bookworm-v20250415"), + DiskSizeGb: new(int64(10)), + DiskType: new("pd-balanced"), + SourceImage: new("projects/debian-cloud/global/images/debian-12-bookworm-v20250415"), }, - Mode: ptr.To("READ_WRITE"), - Type: ptr.To("PERSISTENT"), + Mode: new("READ_WRITE"), + Type: new("PERSISTENT"), // Labels? Tags? }, @@ -209,17 +208,17 @@ func createComputeInstanceTemplate(ctx context.Context, client *compute.Instance { AccessConfigs: []*computepb.AccessConfig{ { - Kind: ptr.To("compute#accessConfig"), - Name: ptr.To("External NAT"), - NetworkTier: ptr.To("PREMIUM"), - Type: ptr.To("ONE_TO_ONE_NAT"), + Kind: new("compute#accessConfig"), + Name: new("External NAT"), + NetworkTier: new("PREMIUM"), + Type: new("ONE_TO_ONE_NAT"), }, }, - Network: ptr.To("projects/" + projectID + "/global/networks/default"), - StackType: ptr.To("IPV4_ONLY"), + Network: new("projects/" + projectID + "/global/networks/default"), + StackType: new("IPV4_ONLY"), }, }, - MachineType: ptr.To("e2-micro"), + MachineType: new("e2-micro"), Tags: &computepb.Tags{ Items: []string{"overmind-test"}, }, @@ -282,13 +281,13 @@ func deleteComputeInstanceTemplate(ctx context.Context, client *compute.Instance func createComputeAutoscaler(ctx context.Context, client *compute.AutoscalersClient, targetedInstanceGroupManager, projectID, zone, name string) error { // Create a new autoscaler autoscaler := &computepb.Autoscaler{ - Name: ptr.To(name), + Name: new(name), Target: &targetedInstanceGroupManager, AutoscalingPolicy: &computepb.AutoscalingPolicy{ - MinNumReplicas: ptr.To(int32(0)), - MaxNumReplicas: ptr.To(int32(1)), + MinNumReplicas: new(int32(0)), + MaxNumReplicas: new(int32(1)), CpuUtilization: &computepb.AutoscalingPolicyCpuUtilization{ - UtilizationTarget: ptr.To(float64(0.6)), + UtilizationTarget: new(float64(0.6)), }, }, } diff --git a/sources/gcp/integration-tests/compute-disk_test.go b/sources/gcp/integration-tests/compute-disk_test.go index 58997c1c..09ff0934 100644 --- a/sources/gcp/integration-tests/compute-disk_test.go +++ b/sources/gcp/integration-tests/compute-disk_test.go @@ -12,7 +12,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -129,9 +128,9 @@ func TestComputeDiskIntegration(t *testing.T) { func createDisk(ctx context.Context, client *compute.DisksClient, projectID, zone, diskName string) error { disk := &computepb.Disk{ - Name: ptr.To(diskName), - SizeGb: ptr.To(int64(10)), - Type: ptr.To(fmt.Sprintf( + Name: new(diskName), + SizeGb: new(int64(10)), + Type: new(fmt.Sprintf( "projects/%s/zones/%s/diskTypes/pd-standard", projectID, zone, )), diff --git a/sources/gcp/integration-tests/compute-forwarding-rule_test.go b/sources/gcp/integration-tests/compute-forwarding-rule_test.go index 0b747dbe..b741abad 100644 --- a/sources/gcp/integration-tests/compute-forwarding-rule_test.go +++ b/sources/gcp/integration-tests/compute-forwarding-rule_test.go @@ -12,7 +12,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -122,7 +121,7 @@ func createComputeForwardingRule(ctx context.Context, client *compute.Forwarding Project: projectID, Region: region, ForwardingRuleResource: &computepb.ForwardingRule{ - Name: ptr.To(ruleName), + Name: new(ruleName), // IP address for which this forwarding rule accepts traffic. // When a client sends traffic to this IP address, the forwarding rule directs the traffic to the referenced target or backendService. // While creating a forwarding rule, specifying an IPAddress is required under the following circumstances: @@ -143,9 +142,9 @@ func createComputeForwardingRule(ctx context.Context, client *compute.Forwarding // determine the type of IP address that you can use. // For detailed information, see [IP address specifications](https://cloud.google.com/load-balancing/docs/forwarding-rule-concepts#ip_address_specifications). // When reading an IPAddress, the API always returns the IP address number. - IPAddress: ptr.To("192.168.1.1"), - IPProtocol: ptr.To("TCP"), - PortRange: ptr.To("80-80"), + IPAddress: new("192.168.1.1"), + IPProtocol: new("TCP"), + PortRange: new("80-80"), // The URL of the target resource to receive the matched traffic. // For regional forwarding rules, this target must be in the same region as the forwarding rule. // For global forwarding rules, this target must be a global load balancing resource. @@ -156,7 +155,7 @@ func createComputeForwardingRule(ctx context.Context, client *compute.Forwarding //- all-apis - All supported Google APIs. //- For Private Service Connect forwarding rules that forward traffic to managed services, the target must be a service attachment. //The target is not mutable once set as a service attachment. - Target: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/targetPools/test-target-pool"), + Target: new("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/targetPools/test-target-pool"), }, } diff --git a/sources/gcp/integration-tests/compute-healthcheck_test.go b/sources/gcp/integration-tests/compute-healthcheck_test.go index 4e8638ab..f71c81ac 100644 --- a/sources/gcp/integration-tests/compute-healthcheck_test.go +++ b/sources/gcp/integration-tests/compute-healthcheck_test.go @@ -12,7 +12,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -135,12 +134,12 @@ func TestComputeHealthCheckIntegration(t *testing.T) { // createComputeHealthCheck creates a GCP Compute HealthCheck with the given parameters. func createComputeHealthCheck(ctx context.Context, client *compute.HealthChecksClient, projectID, healthCheckName string) error { healthCheck := &computepb.HealthCheck{ - Name: ptr.To(healthCheckName), - CheckIntervalSec: ptr.To(int32(5)), - TimeoutSec: ptr.To(int32(5)), - Type: ptr.To("TCP"), + Name: new(healthCheckName), + CheckIntervalSec: new(int32(5)), + TimeoutSec: new(int32(5)), + Type: new("TCP"), TcpHealthCheck: &computepb.TCPHealthCheck{ - Port: ptr.To(int32(80)), + Port: new(int32(80)), }, } diff --git a/sources/gcp/integration-tests/compute-image_test.go b/sources/gcp/integration-tests/compute-image_test.go index a58ef552..f0d174b1 100644 --- a/sources/gcp/integration-tests/compute-image_test.go +++ b/sources/gcp/integration-tests/compute-image_test.go @@ -12,7 +12,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -143,8 +142,8 @@ func TestComputeImageIntegration(t *testing.T) { // createComputeImage creates a GCP Compute Image with the given parameters. func createComputeImage(ctx context.Context, client *compute.ImagesClient, projectID, zone, imageName, diskName string) error { image := &computepb.Image{ - Name: ptr.To(imageName), - SourceDisk: ptr.To(fmt.Sprintf( + Name: new(imageName), + SourceDisk: new(fmt.Sprintf( "projects/%s/zones/%s/disks/%s", projectID, zone, diskName, )), 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 e286d301..e1ba2f33 100644 --- a/sources/gcp/integration-tests/compute-instance-group-manager_test.go +++ b/sources/gcp/integration-tests/compute-instance-group-manager_test.go @@ -12,7 +12,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -153,22 +152,22 @@ func TestComputeInstanceGroupManagerIntegration(t *testing.T) { // createInstanceTemplate creates a GCP Compute Engine instance template. func createInstanceTemplate(ctx context.Context, client *compute.InstanceTemplatesClient, projectID, templateName string) error { template := &computepb.InstanceTemplate{ - Name: ptr.To(templateName), + Name: new(templateName), Properties: &computepb.InstanceProperties{ - MachineType: ptr.To("e2-micro"), + MachineType: new("e2-micro"), Disks: []*computepb.AttachedDisk{ { - Boot: ptr.To(true), - AutoDelete: ptr.To(true), - Type: ptr.To("PERSISTENT"), + Boot: new(true), + AutoDelete: new(true), + Type: new("PERSISTENT"), InitializeParams: &computepb.AttachedDiskInitializeParams{ - SourceImage: ptr.To("projects/debian-cloud/global/images/family/debian-11"), + SourceImage: new("projects/debian-cloud/global/images/family/debian-11"), }, }, }, NetworkInterfaces: []*computepb.NetworkInterface{ { - Network: ptr.To("global/networks/default"), + Network: new("global/networks/default"), }, }, }, @@ -224,9 +223,9 @@ func deleteInstanceTemplate(ctx context.Context, client *compute.InstanceTemplat // createInstanceGroupManager creates a GCP Compute Engine instance group manager. func createInstanceGroupManager(ctx context.Context, client *compute.InstanceGroupManagersClient, projectID, zone, instanceGroupManagerName, templateName string) error { instanceGroupManager := &computepb.InstanceGroupManager{ - Name: ptr.To(instanceGroupManagerName), - InstanceTemplate: ptr.To(fmt.Sprintf("projects/%s/global/instanceTemplates/%s", projectID, templateName)), - TargetSize: ptr.To(int32(1)), + Name: new(instanceGroupManagerName), + InstanceTemplate: new(fmt.Sprintf("projects/%s/global/instanceTemplates/%s", projectID, templateName)), + TargetSize: new(int32(1)), } req := &computepb.InsertInstanceGroupManagerRequest{ diff --git a/sources/gcp/integration-tests/compute-instance-group_test.go b/sources/gcp/integration-tests/compute-instance-group_test.go index 8017775e..9164db0e 100644 --- a/sources/gcp/integration-tests/compute-instance-group_test.go +++ b/sources/gcp/integration-tests/compute-instance-group_test.go @@ -12,7 +12,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -131,7 +130,7 @@ func TestComputeInstanceGroupIntegration(t *testing.T) { func createInstanceGroup(ctx context.Context, client *compute.InstanceGroupsClient, projectID, zone, instanceGroupName string) error { instanceGroup := &computepb.InstanceGroup{ - Name: ptr.To(instanceGroupName), + Name: new(instanceGroupName), } req := &computepb.InsertInstanceGroupRequest{ diff --git a/sources/gcp/integration-tests/compute-instance_test.go b/sources/gcp/integration-tests/compute-instance_test.go index 23d4fb87..f0fd7bbb 100644 --- a/sources/gcp/integration-tests/compute-instance_test.go +++ b/sources/gcp/integration-tests/compute-instance_test.go @@ -12,7 +12,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -121,27 +120,27 @@ func TestComputeInstanceIntegration(t *testing.T) { func createComputeInstance(ctx context.Context, client *compute.InstancesClient, projectID, zone, instanceName, network, subnetwork, region string) error { // Construct the network interface networkInterface := &computepb.NetworkInterface{ - StackType: ptr.To("IPV4_ONLY"), + StackType: new("IPV4_ONLY"), } if network != "" { - networkInterface.Network = ptr.To(fmt.Sprintf("projects/%s/global/networks/%s", projectID, network)) + networkInterface.Network = new(fmt.Sprintf("projects/%s/global/networks/%s", projectID, network)) } if subnetwork != "" { - networkInterface.Subnetwork = ptr.To(fmt.Sprintf("projects/%s/regions/%s/subnetworks/%s", projectID, region, subnetwork)) + networkInterface.Subnetwork = new(fmt.Sprintf("projects/%s/regions/%s/subnetworks/%s", projectID, region, subnetwork)) } // Define the instance configuration instance := &computepb.Instance{ - Name: ptr.To(instanceName), - MachineType: ptr.To(fmt.Sprintf("zones/%s/machineTypes/e2-micro", zone)), + Name: new(instanceName), + MachineType: new(fmt.Sprintf("zones/%s/machineTypes/e2-micro", zone)), Disks: []*computepb.AttachedDisk{ { - Boot: ptr.To(true), - AutoDelete: ptr.To(true), + Boot: new(true), + AutoDelete: new(true), InitializeParams: &computepb.AttachedDiskInitializeParams{ - SourceImage: ptr.To("projects/debian-cloud/global/images/debian-12-bookworm-v20250415"), - DiskSizeGb: ptr.To(int64(10)), + SourceImage: new("projects/debian-cloud/global/images/debian-12-bookworm-v20250415"), + DiskSizeGb: new(int64(10)), }, }, }, diff --git a/sources/gcp/integration-tests/compute-instant-snapshot_test.go b/sources/gcp/integration-tests/compute-instant-snapshot_test.go index 09de7f70..9222cc50 100644 --- a/sources/gcp/integration-tests/compute-instant-snapshot_test.go +++ b/sources/gcp/integration-tests/compute-instant-snapshot_test.go @@ -12,7 +12,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -168,8 +167,8 @@ func TestComputeInstantSnapshotIntegration(t *testing.T) { // createInstantSnapshot creates a GCP Compute Instant Snapshot with the given parameters. func createInstantSnapshot(ctx context.Context, client *compute.InstantSnapshotsClient, projectID, zone, snapshotName, diskName string) error { snapshot := &computepb.InstantSnapshot{ - Name: ptr.To(snapshotName), - SourceDisk: ptr.To(diskName), + Name: new(snapshotName), + SourceDisk: new(diskName), Labels: map[string]string{ "test": "integration", }, diff --git a/sources/gcp/integration-tests/compute-machine-image_test.go b/sources/gcp/integration-tests/compute-machine-image_test.go index 59f1ca2b..9ec569ff 100644 --- a/sources/gcp/integration-tests/compute-machine-image_test.go +++ b/sources/gcp/integration-tests/compute-machine-image_test.go @@ -12,7 +12,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -153,8 +152,8 @@ func TestComputeMachineImageIntegration(t *testing.T) { // createComputeMachineImage creates a GCP Compute Machine Image with the given parameters. func createComputeMachineImage(t *testing.T, ctx context.Context, client *compute.MachineImagesClient, projectID, zone, machineImageName, sourceInstanceName string) error { machineImage := &computepb.MachineImage{ - Name: ptr.To(machineImageName), - SourceInstance: ptr.To(fmt.Sprintf( + Name: new(machineImageName), + SourceInstance: new(fmt.Sprintf( "projects/%s/zones/%s/instances/%s", projectID, zone, sourceInstanceName, )), diff --git a/sources/gcp/integration-tests/compute-node-group_test.go b/sources/gcp/integration-tests/compute-node-group_test.go index 63edb298..67e4dd30 100644 --- a/sources/gcp/integration-tests/compute-node-group_test.go +++ b/sources/gcp/integration-tests/compute-node-group_test.go @@ -13,7 +13,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -204,7 +203,7 @@ func TestComputeNodeGroupIntegration(t *testing.T) { ExpectedType: gcpshared.ComputeNodeGroup.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: nodeTemplateName, - ExpectedScope: "*", + ExpectedScope: "*", }, } @@ -274,8 +273,8 @@ func TestComputeNodeGroupIntegration(t *testing.T) { func createComputeNodeTemplate(ctx context.Context, client *compute.NodeTemplatesClient, projectID, region, name string) error { // Create a new node template nodeTemplate := &computepb.NodeTemplate{ - Name: ptr.To(name), - NodeType: ptr.To("c2-node-60-240"), + Name: new(name), + NodeType: new("c2-node-60-240"), } // Create the node template @@ -336,12 +335,12 @@ func deleteComputeNodeTemplate(ctx context.Context, client *compute.NodeTemplate func createComputeNodeGroup(ctx context.Context, client *compute.NodeGroupsClient, nodeTemplate, projectID, zone, name string) error { // Create a new node group nodeGroup := &computepb.NodeGroup{ - Name: ptr.To(name), - NodeTemplate: ptr.To(nodeTemplate), + Name: new(name), + NodeTemplate: new(nodeTemplate), AutoscalingPolicy: &computepb.NodeGroupAutoscalingPolicy{ - Mode: ptr.To(computepb.NodeGroupAutoscalingPolicy_OFF.String()), - MinNodes: ptr.To(int32(0)), - MaxNodes: ptr.To(int32(1)), + Mode: new(computepb.NodeGroupAutoscalingPolicy_OFF.String()), + MinNodes: new(int32(0)), + MaxNodes: new(int32(1)), }, } diff --git a/sources/gcp/integration-tests/compute-reservation_test.go b/sources/gcp/integration-tests/compute-reservation_test.go index 2046f4b2..75543aa4 100644 --- a/sources/gcp/integration-tests/compute-reservation_test.go +++ b/sources/gcp/integration-tests/compute-reservation_test.go @@ -12,7 +12,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -132,12 +131,12 @@ func TestComputeReservationIntegration(t *testing.T) { // createComputeReservation creates a GCP Compute Reservation with the given parameters. func createComputeReservation(ctx context.Context, client *compute.ReservationsClient, projectID, zone, reservationName, machineType string) error { reservation := &computepb.Reservation{ - Name: ptr.To(reservationName), + Name: new(reservationName), SpecificReservation: &computepb.AllocationSpecificSKUReservation{ InstanceProperties: &computepb.AllocationSpecificSKUAllocationReservedInstanceProperties{ - MachineType: ptr.To(machineType), + MachineType: new(machineType), }, - Count: ptr.To(int64(1)), + Count: new(int64(1)), }, } diff --git a/sources/gcp/integration-tests/compute-snapshot_test.go b/sources/gcp/integration-tests/compute-snapshot_test.go index b6a73c28..9a211dbc 100644 --- a/sources/gcp/integration-tests/compute-snapshot_test.go +++ b/sources/gcp/integration-tests/compute-snapshot_test.go @@ -12,7 +12,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdpcache" @@ -143,8 +142,8 @@ func TestComputeSnapshotIntegration(t *testing.T) { // createComputeSnapshot creates a GCP Compute Snapshot with the given parameters. func createComputeSnapshot(ctx context.Context, client *compute.SnapshotsClient, projectID, zone, snapshotName, diskName string) error { snapshot := &computepb.Snapshot{ - Name: ptr.To(snapshotName), - SourceDisk: ptr.To(fmt.Sprintf( + Name: new(snapshotName), + SourceDisk: new(fmt.Sprintf( "projects/%s/zones/%s/disks/%s", projectID, zone, diskName, )), diff --git a/sources/gcp/integration-tests/kms_vs_asset_inventory_test.go b/sources/gcp/integration-tests/kms_vs_asset_inventory_test.go index 0d23eb9e..b35d421d 100644 --- a/sources/gcp/integration-tests/kms_vs_asset_inventory_test.go +++ b/sources/gcp/integration-tests/kms_vs_asset_inventory_test.go @@ -164,7 +164,7 @@ func TestKMSvsAssetInventoryComparison(t *testing.T) { t.Log("") // Asset Inventory may have indexing delay - retry with backoff - var assetResponse map[string]interface{} + var assetResponse map[string]any var assetLatency time.Duration var foundAsset bool @@ -188,10 +188,7 @@ func TestKMSvsAssetInventoryComparison(t *testing.T) { } // Exponential backoff: 5s, 10s, 20s, 40s... up to 60s - waitTime := time.Duration(5*(1<<(attempt-1))) * time.Second - if waitTime > 60*time.Second { - waitTime = 60 * time.Second - } + waitTime := min(time.Duration(5*(1<<(attempt-1)))*time.Second, 60*time.Second) time.Sleep(waitTime) } @@ -332,7 +329,7 @@ func destroyCryptoKeyVersion(ctx context.Context, client *kms.KeyManagementClien } // callKMSDirectAPI calls the Cloud KMS REST API directly to get a CryptoKey. -func callKMSDirectAPI(ctx context.Context, httpClient *http.Client, cryptoKeyName string) (map[string]interface{}, error) { +func callKMSDirectAPI(ctx context.Context, httpClient *http.Client, cryptoKeyName string) (map[string]any, error) { apiURL := fmt.Sprintf("https://cloudkms.googleapis.com/v1/%s", cryptoKeyName) req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil) @@ -356,7 +353,7 @@ func callKMSDirectAPI(ctx context.Context, httpClient *http.Client, cryptoKeyNam return nil, fmt.Errorf("failed to read response body: %w", err) } - var result map[string]interface{} + var result map[string]any if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } @@ -366,7 +363,7 @@ func callKMSDirectAPI(ctx context.Context, httpClient *http.Client, cryptoKeyNam // callAssetInventoryAPI calls the Cloud Asset Inventory API to find a specific CryptoKey. // Returns the asset if found, nil if not found (may indicate indexing delay). -func callAssetInventoryAPI(ctx context.Context, httpClient *http.Client, projectID, cryptoKeyName string) (map[string]interface{}, error) { +func callAssetInventoryAPI(ctx context.Context, httpClient *http.Client, projectID, cryptoKeyName string) (map[string]any, error) { // Build the Asset Inventory ListAssets URL baseURL := fmt.Sprintf("https://cloudasset.googleapis.com/v1/projects/%s/assets", projectID) @@ -401,13 +398,13 @@ func callAssetInventoryAPI(ctx context.Context, httpClient *http.Client, project return nil, fmt.Errorf("failed to read response body: %w", err) } - var result map[string]interface{} + var result map[string]any if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("failed to unmarshal response: %w", err) } // Find the specific CryptoKey in the assets list - assets, ok := result["assets"].([]interface{}) + assets, ok := result["assets"].([]any) if !ok || len(assets) == 0 { return nil, nil // No assets found - may indicate indexing delay } @@ -417,7 +414,7 @@ func callAssetInventoryAPI(ctx context.Context, httpClient *http.Client, project expectedAssetName := fmt.Sprintf("//cloudkms.googleapis.com/%s", cryptoKeyName) for _, asset := range assets { - assetMap, ok := asset.(map[string]interface{}) + assetMap, ok := asset.(map[string]any) if !ok { continue } diff --git a/sources/gcp/integration-tests/service-account-impersonation_test.go b/sources/gcp/integration-tests/service-account-impersonation_test.go index 53b380d8..dec678cf 100644 --- a/sources/gcp/integration-tests/service-account-impersonation_test.go +++ b/sources/gcp/integration-tests/service-account-impersonation_test.go @@ -7,14 +7,15 @@ import ( "fmt" "net/http" "os" + "slices" "strings" "testing" "time" - 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" + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" credentials "cloud.google.com/go/iam/credentials/apiv1" credentialspb "cloud.google.com/go/iam/credentials/apiv1/credentialspb" "github.com/google/uuid" @@ -562,13 +563,7 @@ func grantServiceAccountTokenCreator(ctx context.Context, iamService *iam.Servic for i, binding := range policy.Bindings { if binding.Role == role { // Check if member already exists - memberFound := false - for _, m := range binding.Members { - if m == member { - memberFound = true - break - } - } + memberFound := slices.Contains(binding.Members, member) if !memberFound { policy.Bindings[i].Members = append(policy.Bindings[i].Members, member) } @@ -632,10 +627,8 @@ func verifyServiceAccountTokenCreatorBinding(ctx context.Context, iamService *ia for _, binding := range policy.Bindings { if binding.Role == role { // Check if the impersonator service account is in the members list - for _, m := range binding.Members { - if m == member { - return true, nil - } + if slices.Contains(binding.Members, member) { + return true, nil } } } @@ -686,13 +679,7 @@ func grantProjectIAMRole(ctx context.Context, crmService *cloudresourcemanager.S for i, binding := range policy.Bindings { if binding.Role == role { // Check if member already exists - memberFound := false - for _, m := range binding.Members { - if m == member { - memberFound = true - break - } - } + memberFound := slices.Contains(binding.Members, member) if !memberFound { policy.Bindings[i].Members = append(policy.Bindings[i].Members, member) } 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 e8c8c976..606c572b 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key-version_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key-version_test.go @@ -23,7 +23,7 @@ func TestCloudKMSCryptoKeyVersion(t *testing.T) { defer cache.Clear() // Pre-populate cache with a CryptoKeyVersion item - attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1", "uniqueAttr": "us|test-keyring|test-key|1", "state": "ENABLED", @@ -105,13 +105,13 @@ func TestCloudKMSCryptoKeyVersion(t *testing.T) { defer cache.Clear() // Pre-populate cache with CryptoKeyVersion items under SEARCH cache key (by cryptoKey) - attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs1, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1", "uniqueAttr": "us|test-keyring|test-key|1", }) _ = attrs1.Set("uniqueAttr", "us|test-keyring|test-key|1") - attrs2, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs2, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/2", "uniqueAttr": "us|test-keyring|test-key|2", }) @@ -256,13 +256,13 @@ func TestCloudKMSCryptoKeyVersion(t *testing.T) { defer cache.Clear() // Pre-populate cache with CryptoKeyVersion items under SEARCH cache key (by cryptoKey) - attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs1, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us-central1/keyRings/my-keyring/cryptoKeys/my-key/cryptoKeyVersions/1", "uniqueAttr": "us-central1|my-keyring|my-key|1", }) _ = attrs1.Set("uniqueAttr", "us-central1|my-keyring|my-key|1") - attrs2, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs2, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us-central1/keyRings/my-keyring/cryptoKeys/my-key/cryptoKeyVersions/2", "uniqueAttr": "us-central1|my-keyring|my-key|2", }) @@ -332,7 +332,7 @@ func TestCloudKMSCryptoKeyVersion(t *testing.T) { defer cache.Clear() // Pre-populate cache with CryptoKeyVersion items - attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs1, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/europe-west1/keyRings/prod-keyring/cryptoKeys/prod-key/cryptoKeyVersions/1", "uniqueAttr": "europe-west1|prod-keyring|prod-key|1", }) @@ -376,7 +376,7 @@ func TestCloudKMSCryptoKeyVersion(t *testing.T) { defer cache.Clear() // Pre-populate cache with a CryptoKeyVersion item with linked queries - attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/1", "uniqueAttr": "us|test-keyring|test-key|1", }) diff --git a/sources/gcp/manual/cloud-kms-crypto-key_test.go b/sources/gcp/manual/cloud-kms-crypto-key_test.go index 4bd7195f..b2e98b7b 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key_test.go @@ -23,7 +23,7 @@ func TestCloudKMSCryptoKey(t *testing.T) { defer cache.Clear() // Pre-populate cache with a CryptoKey item - attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key", "uniqueAttr": "global|test-keyring|test-key", }) @@ -104,13 +104,13 @@ func TestCloudKMSCryptoKey(t *testing.T) { defer cache.Clear() // Pre-populate cache with CryptoKey items under SEARCH cache key (by keyRing) - attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs1, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key-1", "uniqueAttr": "global|test-keyring|test-key-1", }) _ = attrs1.Set("uniqueAttr", "global|test-keyring|test-key-1") - attrs2, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs2, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key-2", "uniqueAttr": "global|test-keyring|test-key-2", }) @@ -238,7 +238,7 @@ func TestCloudKMSCryptoKey(t *testing.T) { // Pre-populate cache with a specific CryptoKey item // Note: Terraform queries with full path are converted to GET operations by the adapter framework - attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us-central1/keyRings/my-keyring/cryptoKeys/my-key-1", "uniqueAttr": "us-central1|my-keyring|my-key-1", }) @@ -293,13 +293,13 @@ func TestCloudKMSCryptoKey(t *testing.T) { defer cache.Clear() // Pre-populate cache with CryptoKey items - attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs1, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us-central1/keyRings/my-keyring/cryptoKeys/my-key-1", "uniqueAttr": "us-central1|my-keyring|my-key-1", }) _ = attrs1.Set("uniqueAttr", "us-central1|my-keyring|my-key-1") - attrs2, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs2, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us-central1/keyRings/my-keyring/cryptoKeys/my-key-2", "uniqueAttr": "us-central1|my-keyring|my-key-2", }) @@ -375,7 +375,7 @@ func TestCloudKMSCryptoKey(t *testing.T) { defer cache.Clear() // Pre-populate cache with a CryptoKey item with linked queries - attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key", "uniqueAttr": "global|test-keyring|test-key", }) diff --git a/sources/gcp/manual/cloud-kms-key-ring_test.go b/sources/gcp/manual/cloud-kms-key-ring_test.go index 713d169b..f2cd5398 100644 --- a/sources/gcp/manual/cloud-kms-key-ring_test.go +++ b/sources/gcp/manual/cloud-kms-key-ring_test.go @@ -23,7 +23,7 @@ func TestCloudKMSKeyRing(t *testing.T) { defer cache.Clear() // Pre-populate cache with a KeyRing item (simulating what the loader would do) - attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us/keyRings/test-keyring", "uniqueAttr": "us|test-keyring", }) @@ -99,13 +99,13 @@ func TestCloudKMSKeyRing(t *testing.T) { defer cache.Clear() // Pre-populate cache with KeyRing items under LIST cache key - attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs1, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us/keyRings/test-keyring-1", "uniqueAttr": "us|test-keyring-1", }) _ = attrs1.Set("uniqueAttr", "us|test-keyring-1") - attrs2, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs2, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us/keyRings/test-keyring-2", "uniqueAttr": "us|test-keyring-2", }) @@ -230,7 +230,7 @@ func TestCloudKMSKeyRing(t *testing.T) { defer cache.Clear() // Pre-populate cache with KeyRing items under SEARCH cache key (by location) - attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us/keyRings/test-keyring", "uniqueAttr": "us|test-keyring", }) @@ -316,7 +316,7 @@ func TestCloudKMSKeyRing(t *testing.T) { defer cache.Clear() // Pre-populate cache with KeyRing item - attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us-central1/keyRings/my-keyring", "uniqueAttr": "us-central1|my-keyring", }) @@ -370,7 +370,7 @@ func TestCloudKMSKeyRing(t *testing.T) { defer cache.Clear() // Pre-populate cache with KeyRing item - attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us-central1/keyRings/my-keyring", "uniqueAttr": "us-central1|my-keyring", }) @@ -423,7 +423,7 @@ func TestCloudKMSKeyRing(t *testing.T) { defer cache.Clear() // Pre-populate cache with a KeyRing item - attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "projects/test-project-id/locations/us/keyRings/test-keyring", "uniqueAttr": "us|test-keyring", }) diff --git a/sources/gcp/manual/compute-address.go b/sources/gcp/manual/compute-address.go index 3e5f8735..fcbf5f4a 100644 --- a/sources/gcp/manual/compute-address.go +++ b/sources/gcp/manual/compute-address.go @@ -10,7 +10,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "google.golang.org/protobuf/proto" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -188,7 +187,7 @@ func (c computeAddressWrapper) listAggregatedStream(ctx context.Context, stream p.Go(func(ctx context.Context) error { it := c.client.AggregatedList(ctx, &computepb.AggregatedListAddressesRequest{ Project: projectID, - ReturnPartialSuccess: proto.Bool(true), // Handle partial failures gracefully + ReturnPartialSuccess: new(true), // Handle partial failures gracefully }) for { diff --git a/sources/gcp/manual/compute-address_test.go b/sources/gcp/manual/compute-address_test.go index 8171cd94..19e60358 100644 --- a/sources/gcp/manual/compute-address_test.go +++ b/sources/gcp/manual/compute-address_test.go @@ -10,7 +10,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -382,11 +381,11 @@ func TestComputeAddress(t *testing.T) { func createComputeAddress(addressName string) *computepb.Address { return &computepb.Address{ - Name: ptr.To(addressName), + Name: new(addressName), Labels: map[string]string{"env": "test"}, - Network: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/global/networks/network"), - Subnetwork: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/subnetworks/default"), - Address: ptr.To("192.168.1.3"), + Network: new("https://www.googleapis.com/compute/v1/projects/test-project-id/global/networks/network"), + Subnetwork: new("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/subnetworks/default"), + Address: new("192.168.1.3"), } } @@ -398,6 +397,6 @@ func createComputeAddressWithUsers(addressName string, users []string) *computep func createComputeAddressWithIPCollection(addressName string, ipCollection string) *computepb.Address { addr := createComputeAddress(addressName) - addr.IpCollection = ptr.To(ipCollection) + addr.IpCollection = new(ipCollection) return addr } diff --git a/sources/gcp/manual/compute-autoscaler.go b/sources/gcp/manual/compute-autoscaler.go index 18923b2a..f2b21eac 100644 --- a/sources/gcp/manual/compute-autoscaler.go +++ b/sources/gcp/manual/compute-autoscaler.go @@ -9,7 +9,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "google.golang.org/protobuf/proto" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -181,7 +180,7 @@ func (c computeAutoscalerWrapper) listAggregatedStream(ctx context.Context, stre p.Go(func(ctx context.Context) error { it := c.client.AggregatedList(ctx, &computepb.AggregatedListAutoscalersRequest{ Project: projectID, - ReturnPartialSuccess: proto.Bool(true), // Handle partial failures gracefully + ReturnPartialSuccess: new(true), // Handle partial failures gracefully }) for { diff --git a/sources/gcp/manual/compute-autoscaler_test.go b/sources/gcp/manual/compute-autoscaler_test.go index 67ab1aa3..65d3e43c 100644 --- a/sources/gcp/manual/compute-autoscaler_test.go +++ b/sources/gcp/manual/compute-autoscaler_test.go @@ -9,7 +9,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -256,15 +255,15 @@ func TestComputeAutoscalerWrapper(t *testing.T) { // Create an autoscaler fixture (as returned from GCP API). func createAutoscalerApiFixture(autoscalerName string) *computepb.Autoscaler { return &computepb.Autoscaler{ - Name: ptr.To(autoscalerName), - Target: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a/instanceGroupManagers/test-instance-group"), + Name: new(autoscalerName), + Target: new("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a/instanceGroupManagers/test-instance-group"), AutoscalingPolicy: &computepb.AutoscalingPolicy{ - MinNumReplicas: ptr.To(int32(1)), - MaxNumReplicas: ptr.To(int32(5)), + MinNumReplicas: new(int32(1)), + MaxNumReplicas: new(int32(5)), CpuUtilization: &computepb.AutoscalingPolicyCpuUtilization{ - UtilizationTarget: ptr.To(float64(0.6)), + UtilizationTarget: new(float64(0.6)), }, }, - Zone: ptr.To("us-central1-a"), + Zone: new("us-central1-a"), } } diff --git a/sources/gcp/manual/compute-backend-service.go b/sources/gcp/manual/compute-backend-service.go index d61a47dd..3905387d 100644 --- a/sources/gcp/manual/compute-backend-service.go +++ b/sources/gcp/manual/compute-backend-service.go @@ -4,13 +4,13 @@ import ( "context" "errors" "fmt" + "slices" "strings" "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "google.golang.org/protobuf/proto" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -66,10 +66,8 @@ func (c computeBackendServiceWrapper) validateAndParseScope(scope string) (gcpsh allLocations := append([]gcpshared.LocationInfo{}, c.projectLocations...) allLocations = append(allLocations, c.regionLocations...) - for _, configuredLoc := range allLocations { - if location.Equals(configuredLoc) { - return location, nil - } + if slices.ContainsFunc(allLocations, location.Equals) { + return location, nil } return gcpshared.LocationInfo{}, &sdp.QueryError{ @@ -275,7 +273,7 @@ func (c computeBackendServiceWrapper) listAggregatedStream(ctx context.Context, p.Go(func(ctx context.Context) error { it := c.globalClient.AggregatedList(ctx, &computepb.AggregatedListBackendServicesRequest{ Project: projectID, - ReturnPartialSuccess: proto.Bool(true), // Handle partial failures gracefully + ReturnPartialSuccess: new(true), // Handle partial failures gracefully }) for { diff --git a/sources/gcp/manual/compute-backend-service_test.go b/sources/gcp/manual/compute-backend-service_test.go index 0a781f0f..f01d4fad 100644 --- a/sources/gcp/manual/compute-backend-service_test.go +++ b/sources/gcp/manual/compute-backend-service_test.go @@ -11,7 +11,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -439,7 +438,7 @@ func TestComputeBackendService(t *testing.T) { backendService := createComputeBackendService("test-backend-service") backendService.Backends = []*computepb.Backend{ { - Group: ptr.To(instanceGroupURL), + Group: new(instanceGroupURL), }, } @@ -512,9 +511,9 @@ func TestComputeBackendService(t *testing.T) { backendService := createComputeBackendService("test-backend-service") backendService.HaPolicy = &computepb.BackendServiceHAPolicy{ Leader: &computepb.BackendServiceHAPolicyLeader{ - BackendGroup: ptr.To(backendGroupURL), + BackendGroup: new(backendGroupURL), NetworkEndpoint: &computepb.BackendServiceHAPolicyLeaderNetworkEndpoint{ - Instance: ptr.To(instanceName), + Instance: new(instanceName), }, }, } @@ -591,7 +590,7 @@ func TestComputeBackendService(t *testing.T) { region := "us-central1" regionURL := fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/regions/%s", projectID, region) backendService := createComputeBackendService("test-backend-service") - backendService.Region = ptr.To(regionURL) + backendService.Region = new(regionURL) mockGlobalClient.EXPECT().Get(ctx, gomock.Any()).Return(backendService, nil) @@ -726,14 +725,14 @@ func TestComputeBackendService(t *testing.T) { func createComputeBackendService(name string) *computepb.BackendService { return &computepb.BackendService{ - Name: ptr.To(name), - Network: ptr.To("global/networks/network"), - SecurityPolicy: ptr.To("https://compute.googleapis.com/compute/v1/projects/test-project/global/securityPolicies/test-security-policy"), - EdgeSecurityPolicy: ptr.To("https://compute.googleapis.com/compute/v1/projects/test-project/global/securityPolicies/test-edge-security-policy"), + Name: new(name), + Network: new("global/networks/network"), + SecurityPolicy: new("https://compute.googleapis.com/compute/v1/projects/test-project/global/securityPolicies/test-security-policy"), + EdgeSecurityPolicy: new("https://compute.googleapis.com/compute/v1/projects/test-project/global/securityPolicies/test-edge-security-policy"), SecuritySettings: &computepb.SecuritySettings{ - ClientTlsPolicy: ptr.To("https://networksecurity.googleapis.com/v1/projects/test-project/locations/test-location/clientTlsPolicies/test-client-tls-policy"), + ClientTlsPolicy: new("https://networksecurity.googleapis.com/v1/projects/test-project/locations/test-location/clientTlsPolicies/test-client-tls-policy"), }, - ServiceLbPolicy: ptr.To(" https://networkservices.googleapis.com/v1alpha1/name=projects/test-project/locations/test-location/serviceLbPolicies/test-service-lb-policy"), + ServiceLbPolicy: new(" https://networkservices.googleapis.com/v1alpha1/name=projects/test-project/locations/test-location/serviceLbPolicies/test-service-lb-policy"), ServiceBindings: []string{ "https://networkservices.googleapis.com/v1alpha1/projects/test-project/locations/test-location/serviceBindings/test-service-binding", }, diff --git a/sources/gcp/manual/compute-disk.go b/sources/gcp/manual/compute-disk.go index 697e074c..116e2b14 100644 --- a/sources/gcp/manual/compute-disk.go +++ b/sources/gcp/manual/compute-disk.go @@ -9,7 +9,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "google.golang.org/protobuf/proto" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -186,7 +185,7 @@ func (c computeDiskWrapper) listAggregatedStream(ctx context.Context, stream dis p.Go(func(ctx context.Context) error { it := c.client.AggregatedList(ctx, &computepb.AggregatedListDisksRequest{ Project: projectID, - ReturnPartialSuccess: proto.Bool(true), // Handle partial failures gracefully + ReturnPartialSuccess: new(true), // Handle partial failures gracefully }) for { diff --git a/sources/gcp/manual/compute-disk_test.go b/sources/gcp/manual/compute-disk_test.go index 4fcd801d..11b44819 100644 --- a/sources/gcp/manual/compute-disk_test.go +++ b/sources/gcp/manual/compute-disk_test.go @@ -10,7 +10,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -63,10 +62,10 @@ func TestComputeDisk(t *testing.T) { sourceValue: "projects/test-project-id/global/images/test-image", expectedLinked: shared.QueryTests{ { - ExpectedType: gcpshared.ComputeImage.String(), - ExpectedMethod: sdp.QueryMethod_SEARCH, - ExpectedQuery: "projects/test-project-id/global/images/test-image", - ExpectedScope: "test-project-id", + ExpectedType: gcpshared.ComputeImage.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "projects/test-project-id/global/images/test-image", + ExpectedScope: "test-project-id", }, }, }, @@ -76,10 +75,10 @@ func TestComputeDisk(t *testing.T) { sourceValue: "projects/test-project-id/global/snapshots/test-snapshot", expectedLinked: shared.QueryTests{ { - ExpectedType: gcpshared.ComputeSnapshot.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-snapshot", - ExpectedScope: "test-project-id", + ExpectedType: gcpshared.ComputeSnapshot.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-snapshot", + ExpectedScope: "test-project-id", }, }, }, @@ -89,10 +88,10 @@ func TestComputeDisk(t *testing.T) { sourceValue: "projects/test-project-id/zones/us-central1-a/instantSnapshots/test-instant-snapshot", expectedLinked: shared.QueryTests{ { - ExpectedType: gcpshared.ComputeInstantSnapshot.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-instant-snapshot", - ExpectedScope: "test-project-id.us-central1-a", + ExpectedType: gcpshared.ComputeInstantSnapshot.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-instant-snapshot", + ExpectedScope: "test-project-id.us-central1-a", }, }, }, @@ -102,10 +101,10 @@ func TestComputeDisk(t *testing.T) { sourceValue: "projects/test-project-id/zones/us-central1-a/disks/source-disk", expectedLinked: shared.QueryTests{ { - ExpectedType: gcpshared.ComputeDisk.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "source-disk", - ExpectedScope: "test-project-id.us-central1-a", + ExpectedType: gcpshared.ComputeDisk.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "source-disk", + ExpectedScope: "test-project-id.us-central1-a", }, }, }, @@ -113,46 +112,46 @@ func TestComputeDisk(t *testing.T) { // These are always present resourcePolicyTest := shared.QueryTest{ - ExpectedType: gcpshared.ComputeResourcePolicy.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-policy", - ExpectedScope: "test-project-id.us-central1", + ExpectedType: gcpshared.ComputeResourcePolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-policy", + ExpectedScope: "test-project-id.us-central1", } userTest := shared.QueryTest{ - ExpectedType: gcpshared.ComputeInstance.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-instance", - ExpectedScope: "test-project-id.us-central1-a", + ExpectedType: gcpshared.ComputeInstance.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-instance", + ExpectedScope: "test-project-id.us-central1-a", } diskTypeTest := shared.QueryTest{ - ExpectedType: gcpshared.ComputeDiskType.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "pd-standard", - ExpectedScope: "test-project-id.us-central1-a", + ExpectedType: gcpshared.ComputeDiskType.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "pd-standard", + ExpectedScope: "test-project-id.us-central1-a", } 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", + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", + ExpectedScope: "test-project-id", } 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", + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", + ExpectedScope: "test-project-id", } 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", + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", + ExpectedScope: "test-project-id", } sourceConsistencyGroupPolicy := shared.QueryTest{ - ExpectedType: gcpshared.ComputeResourcePolicy.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-consistency-group-policy", - ExpectedScope: "test-project-id.us-central1", + ExpectedType: gcpshared.ComputeResourcePolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-consistency-group-policy", + ExpectedScope: "test-project-id.us-central1", } for _, tc := range cases { @@ -405,7 +404,7 @@ func TestComputeDisk(t *testing.T) { // Test with gs:// URI format sourceStorageObject := "gs://test-bucket/path/to/image.tar.gz" disk := createComputeDisk("test-disk", computepb.Disk_READY) - disk.SourceStorageObject = ptr.To(sourceStorageObject) + disk.SourceStorageObject = new(sourceStorageObject) mockClient.EXPECT().Get(ctx, gomock.Any()).Return(disk, nil) @@ -420,61 +419,61 @@ func TestComputeDisk(t *testing.T) { // Base queries that are always present baseQueries := shared.QueryTests{ { - ExpectedType: gcpshared.ComputeResourcePolicy.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-policy", - ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), + ExpectedType: gcpshared.ComputeResourcePolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-policy", + ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), }, { - ExpectedType: gcpshared.ComputeInstance.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-instance", - ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), + ExpectedType: gcpshared.ComputeInstance.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-instance", + ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), }, { - ExpectedType: gcpshared.ComputeDiskType.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "pd-standard", - ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), + ExpectedType: gcpshared.ComputeDiskType.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "pd-standard", + ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), }, { - ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", - ExpectedScope: projectID, + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", + ExpectedScope: projectID, }, { - ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", - ExpectedScope: projectID, + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", + ExpectedScope: projectID, }, { - ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", - ExpectedScope: projectID, + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", + ExpectedScope: projectID, }, { - ExpectedType: gcpshared.ComputeResourcePolicy.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-consistency-group-policy", - ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), + ExpectedType: gcpshared.ComputeResourcePolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-consistency-group-policy", + ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), }, { - ExpectedType: gcpshared.ComputeImage.String(), - ExpectedMethod: sdp.QueryMethod_SEARCH, - ExpectedQuery: "projects/test-project-id/global/images/test-image", - ExpectedScope: projectID, + ExpectedType: gcpshared.ComputeImage.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "projects/test-project-id/global/images/test-image", + ExpectedScope: projectID, }, } // Add the new query we're testing queryTests := append(baseQueries, shared.QueryTest{ - ExpectedType: gcpshared.StorageBucket.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-bucket", - ExpectedScope: projectID, + ExpectedType: gcpshared.StorageBucket.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-bucket", + ExpectedScope: projectID, }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -486,7 +485,7 @@ func TestComputeDisk(t *testing.T) { storagePoolURL := fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/zones/%s/storagePools/test-storage-pool", projectID, zone) disk := createComputeDisk("test-disk", computepb.Disk_READY) - disk.StoragePool = ptr.To(storagePoolURL) + disk.StoragePool = new(storagePoolURL) mockClient.EXPECT().Get(ctx, gomock.Any()).Return(disk, nil) @@ -501,61 +500,61 @@ func TestComputeDisk(t *testing.T) { // Base queries that are always present (same as above) baseQueries := shared.QueryTests{ { - ExpectedType: gcpshared.ComputeResourcePolicy.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-policy", - ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), + ExpectedType: gcpshared.ComputeResourcePolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-policy", + ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), }, { - ExpectedType: gcpshared.ComputeInstance.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-instance", - ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), + ExpectedType: gcpshared.ComputeInstance.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-instance", + ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), }, { - ExpectedType: gcpshared.ComputeDiskType.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "pd-standard", - ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), + ExpectedType: gcpshared.ComputeDiskType.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "pd-standard", + ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), }, { - ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", - ExpectedScope: projectID, + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", + ExpectedScope: projectID, }, { - ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", - ExpectedScope: projectID, + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", + ExpectedScope: projectID, }, { - ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", - ExpectedScope: projectID, + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", + ExpectedScope: projectID, }, { - ExpectedType: gcpshared.ComputeResourcePolicy.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-consistency-group-policy", - ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), + ExpectedType: gcpshared.ComputeResourcePolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-consistency-group-policy", + ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), }, { - ExpectedType: gcpshared.ComputeImage.String(), - ExpectedMethod: sdp.QueryMethod_SEARCH, - ExpectedQuery: "projects/test-project-id/global/images/test-image", - ExpectedScope: projectID, + ExpectedType: gcpshared.ComputeImage.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "projects/test-project-id/global/images/test-image", + ExpectedScope: projectID, }, } // Add the new query we're testing queryTests := append(baseQueries, shared.QueryTest{ - ExpectedType: gcpshared.ComputeStoragePool.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-storage-pool", - ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), + ExpectedType: gcpshared.ComputeStoragePool.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-storage-pool", + ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), }) shared.RunStaticTests(t, adapter, sdpItem, queryTests) @@ -569,8 +568,8 @@ func TestComputeDisk(t *testing.T) { consistencyGroupPolicyURL := fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/regions/us-central1/resourcePolicies/test-consistency-policy", projectID) disk := createComputeDisk("test-disk", computepb.Disk_READY) disk.AsyncPrimaryDisk = &computepb.DiskAsyncReplication{ - Disk: ptr.To(primaryDiskURL), - ConsistencyGroupPolicy: ptr.To(consistencyGroupPolicyURL), + Disk: new(primaryDiskURL), + ConsistencyGroupPolicy: new(consistencyGroupPolicyURL), } mockClient.EXPECT().Get(ctx, gomock.Any()).Return(disk, nil) @@ -586,68 +585,68 @@ func TestComputeDisk(t *testing.T) { // Base queries that are always present baseQueries := shared.QueryTests{ { - ExpectedType: gcpshared.ComputeResourcePolicy.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-policy", - ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), + ExpectedType: gcpshared.ComputeResourcePolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-policy", + ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), }, { - ExpectedType: gcpshared.ComputeInstance.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-instance", - ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), + ExpectedType: gcpshared.ComputeInstance.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-instance", + ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), }, { - ExpectedType: gcpshared.ComputeDiskType.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "pd-standard", - ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), + ExpectedType: gcpshared.ComputeDiskType.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "pd-standard", + ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), }, { - ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", - ExpectedScope: projectID, + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", + ExpectedScope: projectID, }, { - ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", - ExpectedScope: projectID, + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", + ExpectedScope: projectID, }, { - ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", - ExpectedScope: projectID, + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", + ExpectedScope: projectID, }, { - ExpectedType: gcpshared.ComputeResourcePolicy.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-consistency-group-policy", - ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), + ExpectedType: gcpshared.ComputeResourcePolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-consistency-group-policy", + ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), }, { - ExpectedType: gcpshared.ComputeImage.String(), - ExpectedMethod: sdp.QueryMethod_SEARCH, - ExpectedQuery: "projects/test-project-id/global/images/test-image", - ExpectedScope: projectID, + ExpectedType: gcpshared.ComputeImage.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "projects/test-project-id/global/images/test-image", + ExpectedScope: projectID, }, } // Add the new queries we're testing queryTests := append(baseQueries, shared.QueryTest{ - ExpectedType: gcpshared.ComputeDisk.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "primary-disk", - ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), + ExpectedType: gcpshared.ComputeDisk.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "primary-disk", + ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), }, shared.QueryTest{ - ExpectedType: gcpshared.ComputeResourcePolicy.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-consistency-policy", - ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), + ExpectedType: gcpshared.ComputeResourcePolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-consistency-policy", + ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), }, ) @@ -665,13 +664,13 @@ func TestComputeDisk(t *testing.T) { disk.AsyncSecondaryDisks = map[string]*computepb.DiskAsyncReplicationList{ "secondary-disk-1": { AsyncReplicationDisk: &computepb.DiskAsyncReplication{ - Disk: ptr.To(secondaryDisk1URL), - ConsistencyGroupPolicy: ptr.To(consistencyGroupPolicyURL), + Disk: new(secondaryDisk1URL), + ConsistencyGroupPolicy: new(consistencyGroupPolicyURL), }, }, "secondary-disk-2": { AsyncReplicationDisk: &computepb.DiskAsyncReplication{ - Disk: ptr.To(secondaryDisk2URL), + Disk: new(secondaryDisk2URL), }, }, } @@ -689,74 +688,74 @@ func TestComputeDisk(t *testing.T) { // Base queries that are always present baseQueries := shared.QueryTests{ { - ExpectedType: gcpshared.ComputeResourcePolicy.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-policy", - ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), + ExpectedType: gcpshared.ComputeResourcePolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-policy", + ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), }, { - ExpectedType: gcpshared.ComputeInstance.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-instance", - ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), + ExpectedType: gcpshared.ComputeInstance.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-instance", + ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), }, { - ExpectedType: gcpshared.ComputeDiskType.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "pd-standard", - ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), + ExpectedType: gcpshared.ComputeDiskType.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "pd-standard", + ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), }, { - ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", - ExpectedScope: projectID, + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-disk", + ExpectedScope: projectID, }, { - ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", - ExpectedScope: projectID, + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-image", + ExpectedScope: projectID, }, { - ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", - ExpectedScope: projectID, + ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "global|test-keyring|test-key|test-version-source-snapshot", + ExpectedScope: projectID, }, { - ExpectedType: gcpshared.ComputeResourcePolicy.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-consistency-group-policy", - ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), + ExpectedType: gcpshared.ComputeResourcePolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-consistency-group-policy", + ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), }, { - ExpectedType: gcpshared.ComputeImage.String(), - ExpectedMethod: sdp.QueryMethod_SEARCH, - ExpectedQuery: "projects/test-project-id/global/images/test-image", - ExpectedScope: projectID, + ExpectedType: gcpshared.ComputeImage.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "projects/test-project-id/global/images/test-image", + ExpectedScope: projectID, }, } // Add the new queries we're testing queryTests := append(baseQueries, shared.QueryTest{ - ExpectedType: gcpshared.ComputeDisk.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "secondary-disk-1", - ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), + ExpectedType: gcpshared.ComputeDisk.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "secondary-disk-1", + ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), }, shared.QueryTest{ - ExpectedType: gcpshared.ComputeResourcePolicy.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-consistency-policy", - ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), + ExpectedType: gcpshared.ComputeResourcePolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-consistency-policy", + ExpectedScope: fmt.Sprintf("%s.us-central1", projectID), }, shared.QueryTest{ - ExpectedType: gcpshared.ComputeDisk.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "secondary-disk-2", - ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), + ExpectedType: gcpshared.ComputeDisk.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "secondary-disk-2", + ExpectedScope: fmt.Sprintf("%s.%s", projectID, zone), }, ) @@ -912,39 +911,39 @@ func createComputeDisk(diskName string, status computepb.Disk_Status) *computepb // sourceValue is the value to set for the source field. func createComputeDiskWithSource(diskName string, status computepb.Disk_Status, sourceType, sourceValue string) *computepb.Disk { disk := &computepb.Disk{ - Name: ptr.To(diskName), + Name: new(diskName), Labels: map[string]string{"env": "test"}, - Type: ptr.To("projects/test-project-id/zones/us-central1-a/diskTypes/pd-standard"), - Status: ptr.To(status.String()), + Type: new("projects/test-project-id/zones/us-central1-a/diskTypes/pd-standard"), + Status: new(status.String()), ResourcePolicies: []string{"projects/test-project-id/regions/us-central1/resourcePolicies/test-policy"}, Users: []string{"projects/test-project-id/zones/us-central1-a/instances/test-instance"}, DiskEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-disk"), - RawKey: ptr.To("test-key"), + KmsKeyName: new("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-disk"), + RawKey: new("test-key"), }, SourceImageEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-image"), - RawKey: ptr.To("test-key"), + KmsKeyName: new("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-image"), + RawKey: new("test-key"), }, SourceSnapshotEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-snapshot"), - RawKey: ptr.To("test-key"), + KmsKeyName: new("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-snapshot"), + RawKey: new("test-key"), }, - SourceConsistencyGroupPolicy: ptr.To("projects/test-project-id/regions/us-central1/resourcePolicies/test-consistency-group-policy"), + SourceConsistencyGroupPolicy: new("projects/test-project-id/regions/us-central1/resourcePolicies/test-consistency-group-policy"), } switch sourceType { case "image": - disk.SourceImage = ptr.To(sourceValue) + disk.SourceImage = new(sourceValue) case "snapshot": - disk.SourceSnapshot = ptr.To(sourceValue) + disk.SourceSnapshot = new(sourceValue) case "instantSnapshot": - disk.SourceInstantSnapshot = ptr.To(sourceValue) + disk.SourceInstantSnapshot = new(sourceValue) case "disk": - disk.SourceDisk = ptr.To(sourceValue) + disk.SourceDisk = new(sourceValue) default: // Default to image if unknown type - disk.SourceImage = ptr.To("projects/test-project-id/global/images/test-image") + disk.SourceImage = new("projects/test-project-id/global/images/test-image") } return disk diff --git a/sources/gcp/manual/compute-forwarding-rule.go b/sources/gcp/manual/compute-forwarding-rule.go index 2eecc0ac..81b51302 100644 --- a/sources/gcp/manual/compute-forwarding-rule.go +++ b/sources/gcp/manual/compute-forwarding-rule.go @@ -9,7 +9,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "google.golang.org/protobuf/proto" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -193,7 +192,7 @@ func (c computeForwardingRuleWrapper) listAggregatedStream(ctx context.Context, p.Go(func(ctx context.Context) error { it := c.client.AggregatedList(ctx, &computepb.AggregatedListForwardingRulesRequest{ Project: projectID, - ReturnPartialSuccess: proto.Bool(true), // Handle partial failures gracefully + ReturnPartialSuccess: new(true), // Handle partial failures gracefully }) for { diff --git a/sources/gcp/manual/compute-forwarding-rule_test.go b/sources/gcp/manual/compute-forwarding-rule_test.go index 976cbea9..8350fb77 100644 --- a/sources/gcp/manual/compute-forwarding-rule_test.go +++ b/sources/gcp/manual/compute-forwarding-rule_test.go @@ -10,7 +10,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -249,7 +248,7 @@ func TestComputeForwardingRule(t *testing.T) { // Test with TargetHttpProxy targetURL := fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/targetHttpProxies/test-target-proxy", projectID) forwardingRule := createForwardingRule("test-rule", projectID, region, "192.168.1.1") - forwardingRule.Target = ptr.To(targetURL) + forwardingRule.Target = new(targetURL) mockClient.EXPECT().Get(ctx, gomock.Any()).Return(forwardingRule, nil) @@ -306,7 +305,7 @@ func TestComputeForwardingRule(t *testing.T) { baseForwardingRuleURL := fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/regions/%s/forwardingRules/base-forwarding-rule", projectID, region) forwardingRule := createForwardingRule("test-rule", projectID, region, "192.168.1.1") - forwardingRule.BaseForwardingRule = ptr.To(baseForwardingRuleURL) + forwardingRule.BaseForwardingRule = new(baseForwardingRuleURL) mockClient.EXPECT().Get(ctx, gomock.Any()).Return(forwardingRule, nil) @@ -363,7 +362,7 @@ func TestComputeForwardingRule(t *testing.T) { ipCollectionURL := fmt.Sprintf("projects/%s/regions/%s/publicDelegatedPrefixes/test-prefix", projectID, region) forwardingRule := createForwardingRule("test-rule", projectID, region, "192.168.1.1") - forwardingRule.IpCollection = ptr.To(ipCollectionURL) + forwardingRule.IpCollection = new(ipCollectionURL) mockClient.EXPECT().Get(ctx, gomock.Any()).Return(forwardingRule, nil) @@ -423,8 +422,8 @@ func TestComputeForwardingRule(t *testing.T) { forwardingRule := createForwardingRule("test-rule", projectID, region, "192.168.1.1") forwardingRule.ServiceDirectoryRegistrations = []*computepb.ForwardingRuleServiceDirectoryRegistration{ { - Namespace: ptr.To(namespaceURL), - Service: ptr.To(serviceName), + Namespace: new(namespaceURL), + Service: new(serviceName), }, } @@ -489,11 +488,11 @@ func TestComputeForwardingRule(t *testing.T) { func createForwardingRule(name, projectID, region, ipAddress string) *computepb.ForwardingRule { return &computepb.ForwardingRule{ - Name: ptr.To(name), - IPAddress: ptr.To(ipAddress), + Name: new(name), + IPAddress: new(ipAddress), Labels: map[string]string{"env": "test"}, - Network: ptr.To(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/networks/test-network", projectID)), - Subnetwork: ptr.To(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/regions/%s/subnetworks/test-subnetwork", projectID, region)), - BackendService: ptr.To(fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/regions/%s/backendServices/backend-service", projectID, region)), + Network: new(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/networks/test-network", projectID)), + Subnetwork: new(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/regions/%s/subnetworks/test-subnetwork", projectID, region)), + BackendService: new(fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/regions/%s/backendServices/backend-service", projectID, region)), } } diff --git a/sources/gcp/manual/compute-healthcheck.go b/sources/gcp/manual/compute-healthcheck.go index b24e0ae6..fa3ed72c 100644 --- a/sources/gcp/manual/compute-healthcheck.go +++ b/sources/gcp/manual/compute-healthcheck.go @@ -5,12 +5,12 @@ import ( "errors" "fmt" "net" + "slices" "sync/atomic" "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "google.golang.org/protobuf/proto" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -67,10 +67,8 @@ func (c computeHealthCheckWrapper) validateAndParseScope(scope string) (gcpshare allLocations := append([]gcpshared.LocationInfo{}, c.projectLocations...) allLocations = append(allLocations, c.regionLocations...) - for _, configuredLoc := range allLocations { - if location.Equals(configuredLoc) { - return location, nil - } + if slices.ContainsFunc(allLocations, location.Equals) { + return location, nil } return gcpshared.LocationInfo{}, &sdp.QueryError{ @@ -269,7 +267,7 @@ func (c computeHealthCheckWrapper) listAggregatedStream(ctx context.Context, str p.Go(func(ctx context.Context) error { it := c.globalClient.AggregatedList(ctx, &computepb.AggregatedListHealthChecksRequest{ Project: projectID, - ReturnPartialSuccess: proto.Bool(true), // Handle partial failures gracefully + ReturnPartialSuccess: new(true), // Handle partial failures gracefully }) for { diff --git a/sources/gcp/manual/compute-healthcheck_test.go b/sources/gcp/manual/compute-healthcheck_test.go index 5df60f4f..ec0839d3 100644 --- a/sources/gcp/manual/compute-healthcheck_test.go +++ b/sources/gcp/manual/compute-healthcheck_test.go @@ -11,7 +11,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -455,52 +454,52 @@ func TestComputeHealthCheck(t *testing.T) { func createHealthCheck(healthCheckName string) *computepb.HealthCheck { return &computepb.HealthCheck{ - Name: ptr.To(healthCheckName), - CheckIntervalSec: ptr.To(int32(5)), - TimeoutSec: ptr.To(int32(5)), - Type: ptr.To("TCP"), + Name: new(healthCheckName), + CheckIntervalSec: new(int32(5)), + TimeoutSec: new(int32(5)), + Type: new("TCP"), TcpHealthCheck: &computepb.TCPHealthCheck{ - Port: ptr.To(int32(80)), + Port: new(int32(80)), }, } } func createHTTPHealthCheck(healthCheckName, host string) *computepb.HealthCheck { return &computepb.HealthCheck{ - Name: ptr.To(healthCheckName), - CheckIntervalSec: ptr.To(int32(5)), - TimeoutSec: ptr.To(int32(5)), - Type: ptr.To("HTTP"), + Name: new(healthCheckName), + CheckIntervalSec: new(int32(5)), + TimeoutSec: new(int32(5)), + Type: new("HTTP"), HttpHealthCheck: &computepb.HTTPHealthCheck{ - Port: ptr.To(int32(80)), - Host: ptr.To(host), - RequestPath: ptr.To("/"), + Port: new(int32(80)), + Host: new(host), + RequestPath: new("/"), }, } } func createHTTPSHealthCheck(healthCheckName, host string) *computepb.HealthCheck { return &computepb.HealthCheck{ - Name: ptr.To(healthCheckName), - CheckIntervalSec: ptr.To(int32(5)), - TimeoutSec: ptr.To(int32(5)), - Type: ptr.To("HTTPS"), + Name: new(healthCheckName), + CheckIntervalSec: new(int32(5)), + TimeoutSec: new(int32(5)), + Type: new("HTTPS"), HttpsHealthCheck: &computepb.HTTPSHealthCheck{ - Port: ptr.To(int32(443)), - Host: ptr.To(host), - RequestPath: ptr.To("/"), + Port: new(int32(443)), + Host: new(host), + RequestPath: new("/"), }, } } func createHealthCheckWithSourceRegions(healthCheckName string, regions []string) *computepb.HealthCheck { return &computepb.HealthCheck{ - Name: ptr.To(healthCheckName), - CheckIntervalSec: ptr.To(int32(30)), - TimeoutSec: ptr.To(int32(5)), - Type: ptr.To("TCP"), + Name: new(healthCheckName), + CheckIntervalSec: new(int32(30)), + TimeoutSec: new(int32(5)), + Type: new("TCP"), TcpHealthCheck: &computepb.TCPHealthCheck{ - Port: ptr.To(int32(80)), + Port: new(int32(80)), }, SourceRegions: regions, } @@ -508,13 +507,13 @@ func createHealthCheckWithSourceRegions(healthCheckName string, regions []string func createRegionalHealthCheck(healthCheckName, region string) *computepb.HealthCheck { return &computepb.HealthCheck{ - Name: ptr.To(healthCheckName), - CheckIntervalSec: ptr.To(int32(5)), - TimeoutSec: ptr.To(int32(5)), - Type: ptr.To("TCP"), + Name: new(healthCheckName), + CheckIntervalSec: new(int32(5)), + TimeoutSec: new(int32(5)), + Type: new("TCP"), TcpHealthCheck: &computepb.TCPHealthCheck{ - Port: ptr.To(int32(80)), + Port: new(int32(80)), }, - Region: ptr.To(region), + Region: new(region), } } diff --git a/sources/gcp/manual/compute-image_test.go b/sources/gcp/manual/compute-image_test.go index 16567d49..a205ad58 100644 --- a/sources/gcp/manual/compute-image_test.go +++ b/sources/gcp/manual/compute-image_test.go @@ -12,7 +12,6 @@ import ( "google.golang.org/api/iterator" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -389,7 +388,7 @@ func TestComputeImage(t *testing.T) { expectedImageName := "test-image-family-20240101" // When searching by name (not URI), Search tries Get first, then falls back to GetFromFamily - mockClient.EXPECT().Get(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetImageRequest, opts ...interface{}) (*computepb.Image, error) { + mockClient.EXPECT().Get(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetImageRequest, opts ...any) (*computepb.Image, error) { if req.GetProject() != projectID { t.Errorf("Expected project %s, got %s", projectID, req.GetProject()) } @@ -399,7 +398,7 @@ func TestComputeImage(t *testing.T) { return nil, status.Error(codes.NotFound, "image not found") }) - mockClient.EXPECT().GetFromFamily(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetFromFamilyImageRequest, opts ...interface{}) (*computepb.Image, error) { + mockClient.EXPECT().GetFromFamily(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetFromFamilyImageRequest, opts ...any) (*computepb.Image, error) { if req.GetProject() != projectID { t.Errorf("Expected project %s, got %s", projectID, req.GetProject()) } @@ -457,7 +456,7 @@ func TestComputeImage(t *testing.T) { familyURI := "projects/" + projectID + "/global/images/family/test-image-family" expectedImageName := "test-image-family-20240101" - mockClient.EXPECT().GetFromFamily(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetFromFamilyImageRequest, opts ...interface{}) (*computepb.Image, error) { + mockClient.EXPECT().GetFromFamily(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetFromFamilyImageRequest, opts ...any) (*computepb.Image, error) { if req.GetProject() != projectID { t.Errorf("Expected project %s, got %s", projectID, req.GetProject()) } @@ -498,7 +497,7 @@ func TestComputeImage(t *testing.T) { imageURI := "projects/" + projectID + "/global/images/test-image-exact" expectedImageName := "test-image-exact" - mockClient.EXPECT().Get(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetImageRequest, opts ...interface{}) (*computepb.Image, error) { + mockClient.EXPECT().Get(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetImageRequest, opts ...any) (*computepb.Image, error) { if req.GetProject() != projectID { t.Errorf("Expected project %s, got %s", projectID, req.GetProject()) } @@ -542,7 +541,7 @@ func TestComputeImage(t *testing.T) { expectedImageName := "test-image-name" // First Get call fails with NotFound - mockClient.EXPECT().Get(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetImageRequest, opts ...interface{}) (*computepb.Image, error) { + mockClient.EXPECT().Get(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetImageRequest, opts ...any) (*computepb.Image, error) { if req.GetProject() != projectID { t.Errorf("Expected project %s, got %s", projectID, req.GetProject()) } @@ -553,7 +552,7 @@ func TestComputeImage(t *testing.T) { }) // Then GetFromFamily succeeds (treating name as family) - mockClient.EXPECT().GetFromFamily(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetFromFamilyImageRequest, opts ...interface{}) (*computepb.Image, error) { + mockClient.EXPECT().GetFromFamily(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetFromFamilyImageRequest, opts ...any) (*computepb.Image, error) { if req.GetProject() != projectID { t.Errorf("Expected project %s, got %s", projectID, req.GetProject()) } @@ -593,7 +592,7 @@ func TestComputeImage(t *testing.T) { exactImageName := "test-image-exact" - mockClient.EXPECT().Get(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetImageRequest, opts ...interface{}) (*computepb.Image, error) { + mockClient.EXPECT().Get(ctx, gomock.Any()).DoAndReturn(func(ctx context.Context, req *computepb.GetImageRequest, opts ...any) (*computepb.Image, error) { if req.GetProject() != projectID { t.Errorf("Expected project %s, got %s", projectID, req.GetProject()) } @@ -625,9 +624,9 @@ func TestComputeImage(t *testing.T) { func createComputeImage(imageName string, status computepb.Image_Status) *computepb.Image { return &computepb.Image{ - Name: ptr.To(imageName), + Name: new(imageName), Labels: map[string]string{"env": "test"}, - Status: ptr.To(status.String()), + Status: new(status.String()), } } @@ -639,9 +638,9 @@ func createComputeImageWithLinks(projectID, imageName string, status computepb.I replacementImageURL := fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/images/test-replacement-image", projectID) return &computepb.Image{ - Name: ptr.To(imageName), + Name: new(imageName), Labels: map[string]string{"env": "test"}, - Status: ptr.To(status.String()), + Status: new(status.String()), SourceDisk: &sourceDiskURL, SourceSnapshot: &sourceSnapshotURL, SourceImage: &sourceImageURL, @@ -650,19 +649,19 @@ func createComputeImageWithLinks(projectID, imageName string, status computepb.I fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/global/licenses/test-license-2", projectID), }, RawDisk: &computepb.RawDisk{ - Source: ptr.To(fmt.Sprintf("gs://%s-raw-disk-bucket/raw-disk.tar.gz", projectID)), + Source: new(fmt.Sprintf("gs://%s-raw-disk-bucket/raw-disk.tar.gz", projectID)), }, ImageEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To(fmt.Sprintf("projects/%s/locations/global/keyRings/test-keyring/cryptoKeys/test-image-key/cryptoKeyVersions/test-version-image", projectID)), - KmsKeyServiceAccount: ptr.To(fmt.Sprintf("projects/%s/serviceAccounts/test-image-kms-sa@%s.iam.gserviceaccount.com", projectID, projectID)), + KmsKeyName: new(fmt.Sprintf("projects/%s/locations/global/keyRings/test-keyring/cryptoKeys/test-image-key/cryptoKeyVersions/test-version-image", projectID)), + KmsKeyServiceAccount: new(fmt.Sprintf("projects/%s/serviceAccounts/test-image-kms-sa@%s.iam.gserviceaccount.com", projectID, projectID)), }, SourceImageEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To(fmt.Sprintf("projects/%s/locations/global/keyRings/test-keyring/cryptoKeys/test-source-image-key/cryptoKeyVersions/test-version-source-image", projectID)), - KmsKeyServiceAccount: ptr.To(fmt.Sprintf("projects/%s/serviceAccounts/test-source-image-kms-sa@%s.iam.gserviceaccount.com", projectID, projectID)), + KmsKeyName: new(fmt.Sprintf("projects/%s/locations/global/keyRings/test-keyring/cryptoKeys/test-source-image-key/cryptoKeyVersions/test-version-source-image", projectID)), + KmsKeyServiceAccount: new(fmt.Sprintf("projects/%s/serviceAccounts/test-source-image-kms-sa@%s.iam.gserviceaccount.com", projectID, projectID)), }, SourceSnapshotEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To(fmt.Sprintf("projects/%s/locations/global/keyRings/test-keyring/cryptoKeys/test-source-snapshot-key/cryptoKeyVersions/test-version-source-snapshot", projectID)), - KmsKeyServiceAccount: ptr.To(fmt.Sprintf("projects/%s/serviceAccounts/test-source-snapshot-kms-sa@%s.iam.gserviceaccount.com", projectID, projectID)), + KmsKeyName: new(fmt.Sprintf("projects/%s/locations/global/keyRings/test-keyring/cryptoKeys/test-source-snapshot-key/cryptoKeyVersions/test-version-source-snapshot", projectID)), + KmsKeyServiceAccount: new(fmt.Sprintf("projects/%s/serviceAccounts/test-source-snapshot-kms-sa@%s.iam.gserviceaccount.com", projectID, projectID)), }, Deprecated: &computepb.DeprecationStatus{ Replacement: &replacementImageURL, diff --git a/sources/gcp/manual/compute-instance-group-manager.go b/sources/gcp/manual/compute-instance-group-manager.go index bb6cf3e5..ef7f5269 100644 --- a/sources/gcp/manual/compute-instance-group-manager.go +++ b/sources/gcp/manual/compute-instance-group-manager.go @@ -8,7 +8,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "google.golang.org/protobuf/proto" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -189,7 +188,7 @@ func (c computeInstanceGroupManagerWrapper) listAggregatedStream(ctx context.Con p.Go(func(ctx context.Context) error { it := c.client.AggregatedList(ctx, &computepb.AggregatedListInstanceGroupManagersRequest{ Project: projectID, - ReturnPartialSuccess: proto.Bool(true), // Handle partial failures gracefully + ReturnPartialSuccess: new(true), // Handle partial failures gracefully }) for { diff --git a/sources/gcp/manual/compute-instance-group-manager_test.go b/sources/gcp/manual/compute-instance-group-manager_test.go index ea85e239..63fcf17b 100644 --- a/sources/gcp/manual/compute-instance-group-manager_test.go +++ b/sources/gcp/manual/compute-instance-group-manager_test.go @@ -10,7 +10,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -150,26 +149,26 @@ func TestComputeInstanceGroupManager(t *testing.T) { t.Run("VersionsWithInstanceTemplates", func(t *testing.T) { // Create IGM with versions array containing multiple templates igm := &computepb.InstanceGroupManager{ - Name: ptr.To("test-instance-group-manager"), + Name: new("test-instance-group-manager"), Status: &computepb.InstanceGroupManagerStatus{ - IsStable: ptr.To(true), + IsStable: new(true), }, Versions: []*computepb.InstanceGroupManagerVersion{ { - Name: ptr.To("canary"), - InstanceTemplate: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/global/instanceTemplates/canary-template"), + Name: new("canary"), + InstanceTemplate: new("https://www.googleapis.com/compute/v1/projects/test-project-id/global/instanceTemplates/canary-template"), }, { - Name: ptr.To("stable"), - InstanceTemplate: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/instanceTemplates/stable-template"), + Name: new("stable"), + InstanceTemplate: new("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/instanceTemplates/stable-template"), }, }, - InstanceGroup: ptr.To("projects/test-project-id/zones/us-central1-a/instanceGroups/test-group"), + InstanceGroup: new("projects/test-project-id/zones/us-central1-a/instanceGroups/test-group"), TargetPools: []string{ "https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/targetPools/test-pool", }, ResourcePolicies: &computepb.InstanceGroupManagerResourcePolicies{ - WorkloadPolicy: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/resourcePolicies/test-policy"), + WorkloadPolicy: new("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/resourcePolicies/test-policy"), }, } @@ -223,24 +222,24 @@ func TestComputeInstanceGroupManager(t *testing.T) { t.Run("AutoHealingPoliciesWithHealthCheck", func(t *testing.T) { // Create IGM with auto-healing policy containing health check igm := &computepb.InstanceGroupManager{ - Name: ptr.To("test-instance-group-manager"), + Name: new("test-instance-group-manager"), Status: &computepb.InstanceGroupManagerStatus{ - IsStable: ptr.To(true), + IsStable: new(true), }, - Zone: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a"), - InstanceTemplate: ptr.To(instanceTemplateName), - InstanceGroup: ptr.To("projects/test-project-id/zones/us-central1-a/instanceGroups/test-group"), + Zone: new("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a"), + InstanceTemplate: new(instanceTemplateName), + InstanceGroup: new("projects/test-project-id/zones/us-central1-a/instanceGroups/test-group"), AutoHealingPolicies: []*computepb.InstanceGroupManagerAutoHealingPolicy{ { - HealthCheck: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/global/healthChecks/test-health-check"), - InitialDelaySec: ptr.To[int32](300), + HealthCheck: new("https://www.googleapis.com/compute/v1/projects/test-project-id/global/healthChecks/test-health-check"), + InitialDelaySec: new(int32(300)), }, }, TargetPools: []string{ "https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/targetPools/test-pool", }, ResourcePolicies: &computepb.InstanceGroupManagerResourcePolicies{ - WorkloadPolicy: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/resourcePolicies/test-policy"), + WorkloadPolicy: new("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/resourcePolicies/test-policy"), }, } @@ -500,18 +499,18 @@ func TestComputeInstanceGroupManager(t *testing.T) { func createInstanceGroupManager(name string, isStable bool, instanceTemplate string) *computepb.InstanceGroupManager { return &computepb.InstanceGroupManager{ - Name: ptr.To(name), + Name: new(name), Status: &computepb.InstanceGroupManagerStatus{ - IsStable: ptr.To(isStable), + IsStable: new(isStable), }, - Zone: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a"), - InstanceTemplate: ptr.To(instanceTemplate), - InstanceGroup: ptr.To("projects/test-project-id/zones/us-central1-a/instanceGroups/test-group"), + Zone: new("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a"), + InstanceTemplate: new(instanceTemplate), + InstanceGroup: new("projects/test-project-id/zones/us-central1-a/instanceGroups/test-group"), TargetPools: []string{ "https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/targetPools/test-pool", }, ResourcePolicies: &computepb.InstanceGroupManagerResourcePolicies{ - WorkloadPolicy: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/resourcePolicies/test-policy"), + WorkloadPolicy: new("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/resourcePolicies/test-policy"), }, } } diff --git a/sources/gcp/manual/compute-instance-group.go b/sources/gcp/manual/compute-instance-group.go index 82184f13..06d1d10c 100644 --- a/sources/gcp/manual/compute-instance-group.go +++ b/sources/gcp/manual/compute-instance-group.go @@ -8,7 +8,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "google.golang.org/protobuf/proto" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -179,7 +178,7 @@ func (c computeInstanceGroupWrapper) listAggregatedStream(ctx context.Context, s p.Go(func(ctx context.Context) error { it := c.client.AggregatedList(ctx, &computepb.AggregatedListInstanceGroupsRequest{ Project: projectID, - ReturnPartialSuccess: proto.Bool(true), // Handle partial failures gracefully + ReturnPartialSuccess: new(true), // Handle partial failures gracefully }) for { diff --git a/sources/gcp/manual/compute-instance-group_test.go b/sources/gcp/manual/compute-instance-group_test.go index 9fcfc6ef..13891ac7 100644 --- a/sources/gcp/manual/compute-instance-group_test.go +++ b/sources/gcp/manual/compute-instance-group_test.go @@ -10,7 +10,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -233,9 +232,9 @@ func TestComputeInstanceGroup(t *testing.T) { func createComputeInstanceGroup(name, network, subnetwork, projectID, zone string) *computepb.InstanceGroup { return &computepb.InstanceGroup{ - Name: ptr.To(name), - Network: ptr.To(fmt.Sprintf("projects/%s/global/networks/%s", projectID, network)), - Subnetwork: ptr.To(fmt.Sprintf("projects/%s/regions/us-central1/subnetworks/%s", projectID, subnetwork)), - Zone: ptr.To(fmt.Sprintf("projects/%s/zones/%s", projectID, zone)), + Name: new(name), + Network: new(fmt.Sprintf("projects/%s/global/networks/%s", projectID, network)), + Subnetwork: new(fmt.Sprintf("projects/%s/regions/us-central1/subnetworks/%s", projectID, subnetwork)), + Zone: new(fmt.Sprintf("projects/%s/zones/%s", projectID, zone)), } } diff --git a/sources/gcp/manual/compute-instance.go b/sources/gcp/manual/compute-instance.go index 02863bc9..9d91004b 100644 --- a/sources/gcp/manual/compute-instance.go +++ b/sources/gcp/manual/compute-instance.go @@ -9,7 +9,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "google.golang.org/protobuf/proto" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -248,7 +247,7 @@ func (c computeInstanceWrapper) listAggregatedStream(ctx context.Context, stream p.Go(func(ctx context.Context) error { it := c.client.AggregatedList(ctx, &computepb.AggregatedListInstancesRequest{ Project: projectID, - ReturnPartialSuccess: proto.Bool(true), // Handle partial failures gracefully + ReturnPartialSuccess: new(true), // Handle partial failures gracefully }) for { diff --git a/sources/gcp/manual/compute-instance_test.go b/sources/gcp/manual/compute-instance_test.go index 4b30387c..5818f8f2 100644 --- a/sources/gcp/manual/compute-instance_test.go +++ b/sources/gcp/manual/compute-instance_test.go @@ -10,7 +10,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -372,16 +371,16 @@ func TestComputeInstance(t *testing.T) { instance := createComputeInstance("test-instance", computepb.Instance_RUNNING) instance.Disks = []*computepb.AttachedDisk{ { - DeviceName: ptr.To("test-disk"), - Source: ptr.To(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/zones/%s/disks/test-instance", projectID, zone)), + DeviceName: new("test-disk"), + Source: new(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/zones/%s/disks/test-instance", projectID, zone)), InitializeParams: &computepb.AttachedDiskInitializeParams{ - SourceImage: ptr.To(sourceImageURL), - SourceSnapshot: ptr.To(sourceSnapshotURL), + SourceImage: new(sourceImageURL), + SourceSnapshot: new(sourceSnapshotURL), SourceImageEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To(sourceImageKeyName), + KmsKeyName: new(sourceImageKeyName), }, SourceSnapshotEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To(sourceSnapshotKeyName), + KmsKeyName: new(sourceSnapshotKeyName), }, }, }, @@ -478,10 +477,10 @@ func TestComputeInstance(t *testing.T) { instance := createComputeInstance("test-instance", computepb.Instance_RUNNING) instance.Disks = []*computepb.AttachedDisk{ { - DeviceName: ptr.To("test-disk"), - Source: ptr.To(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/zones/%s/disks/test-instance", projectID, zone)), + DeviceName: new("test-disk"), + Source: new(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/zones/%s/disks/test-instance", projectID, zone)), DiskEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To(diskKeyName), + KmsKeyName: new(diskKeyName), }, }, } @@ -557,10 +556,10 @@ func TestComputeInstance(t *testing.T) { instance := createComputeInstance("test-instance", computepb.Instance_RUNNING) instance.Disks = []*computepb.AttachedDisk{ { - DeviceName: ptr.To("test-disk"), - Source: ptr.To(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/zones/%s/disks/test-instance", projectID, zone)), + DeviceName: new("test-disk"), + Source: new(fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/zones/%s/disks/test-instance", projectID, zone)), DiskEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To(diskKeyName), + KmsKeyName: new(diskKeyName), }, }, } @@ -636,7 +635,7 @@ func TestComputeInstance(t *testing.T) { instance := createComputeInstance("test-instance", computepb.Instance_RUNNING) instance.ServiceAccounts = []*computepb.ServiceAccount{ { - Email: ptr.To(serviceAccountEmail), + Email: new(serviceAccountEmail), }, } @@ -715,12 +714,12 @@ func TestComputeInstance(t *testing.T) { instance.Metadata = &computepb.Metadata{ Items: []*computepb.Items{ { - Key: ptr.To("instance-template"), - Value: ptr.To(instanceTemplateURI), + Key: new("instance-template"), + Value: new(instanceTemplateURI), }, { - Key: ptr.To("created-by"), - Value: ptr.To(igmURI), + Key: new("created-by"), + Value: new(igmURI), }, }, } @@ -806,8 +805,8 @@ func TestComputeInstance(t *testing.T) { instance.Metadata = &computepb.Metadata{ Items: []*computepb.Items{ { - Key: ptr.To("instance-template"), - Value: ptr.To(instanceTemplateURI), + Key: new("instance-template"), + Value: new(instanceTemplateURI), }, }, } @@ -1081,23 +1080,23 @@ func TestComputeInstance(t *testing.T) { func createComputeInstance(instanceName string, status computepb.Instance_Status) *computepb.Instance { return &computepb.Instance{ - Name: ptr.To(instanceName), + Name: new(instanceName), Labels: map[string]string{"env": "test"}, Disks: []*computepb.AttachedDisk{ { - DeviceName: ptr.To("test-disk"), - Source: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a/disks/test-instance"), + DeviceName: new("test-disk"), + Source: new("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a/disks/test-instance"), }, }, NetworkInterfaces: []*computepb.NetworkInterface{ { - NetworkIP: ptr.To("192.168.1.3"), - Subnetwork: ptr.To("projects/test-project-id/regions/us-central1/subnetworks/default"), - Network: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/global/networks/network"), - Ipv6Address: ptr.To("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), + NetworkIP: new("192.168.1.3"), + Subnetwork: new("projects/test-project-id/regions/us-central1/subnetworks/default"), + Network: new("https://www.googleapis.com/compute/v1/projects/test-project-id/global/networks/network"), + Ipv6Address: new("2001:0db8:85a3:0000:0000:8a2e:0370:7334"), }, }, - Status: ptr.To(status.String()), + Status: new(status.String()), ResourcePolicies: []string{ "projects/test-project-id/regions/us-central1/resourcePolicies/test-policy", }, diff --git a/sources/gcp/manual/compute-instant-snapshot.go b/sources/gcp/manual/compute-instant-snapshot.go index fb9d71e4..7fdb21d7 100644 --- a/sources/gcp/manual/compute-instant-snapshot.go +++ b/sources/gcp/manual/compute-instant-snapshot.go @@ -8,7 +8,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "google.golang.org/protobuf/proto" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -176,7 +175,7 @@ func (c computeInstantSnapshotWrapper) listAggregatedStream(ctx context.Context, p.Go(func(ctx context.Context) error { it := c.client.AggregatedList(ctx, &computepb.AggregatedListInstantSnapshotsRequest{ Project: projectID, - ReturnPartialSuccess: proto.Bool(true), // Handle partial failures gracefully + ReturnPartialSuccess: new(true), // Handle partial failures gracefully }) for { diff --git a/sources/gcp/manual/compute-instant-snapshot_test.go b/sources/gcp/manual/compute-instant-snapshot_test.go index 62813897..fc0ec912 100644 --- a/sources/gcp/manual/compute-instant-snapshot_test.go +++ b/sources/gcp/manual/compute-instant-snapshot_test.go @@ -9,7 +9,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -314,13 +313,13 @@ func TestComputeInstantSnapshot(t *testing.T) { func createComputeInstantSnapshot(snapshotName, zone string, status computepb.InstantSnapshot_Status) *computepb.InstantSnapshot { return &computepb.InstantSnapshot{ - Name: ptr.To(snapshotName), + Name: new(snapshotName), Labels: map[string]string{"env": "test"}, - Status: ptr.To(status.String()), - Zone: ptr.To(zone), - SourceDisk: ptr.To( + Status: new(status.String()), + Zone: new(zone), + SourceDisk: new( "projects/test-project-id/zones/" + zone + "/disks/test-disk", ), - Architecture: ptr.To(computepb.InstantSnapshot_X86_64.String()), + Architecture: new(computepb.InstantSnapshot_X86_64.String()), } } diff --git a/sources/gcp/manual/compute-machine-image_test.go b/sources/gcp/manual/compute-machine-image_test.go index 7aac8391..7f8ab02c 100644 --- a/sources/gcp/manual/compute-machine-image_test.go +++ b/sources/gcp/manual/compute-machine-image_test.go @@ -8,7 +8,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -373,66 +372,66 @@ func TestComputeMachineImage(t *testing.T) { func createComputeMachineImage(imageName string, status computepb.MachineImage_Status) *computepb.MachineImage { return &computepb.MachineImage{ - Name: ptr.To(imageName), + Name: new(imageName), Labels: map[string]string{"env": "test"}, - Status: ptr.To(status.String()), + Status: new(status.String()), InstanceProperties: &computepb.InstanceProperties{ NetworkInterfaces: []*computepb.NetworkInterface{ { - Network: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/global/networks/test-network"), - Subnetwork: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/subnetworks/test-subnetwork"), - NetworkAttachment: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/networkAttachments/test-network-attachment"), - NetworkIP: ptr.To("10.0.0.1"), - Ipv6Address: ptr.To("2001:db8::1"), + Network: new("https://www.googleapis.com/compute/v1/projects/test-project-id/global/networks/test-network"), + Subnetwork: new("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/subnetworks/test-subnetwork"), + NetworkAttachment: new("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/networkAttachments/test-network-attachment"), + NetworkIP: new("10.0.0.1"), + Ipv6Address: new("2001:db8::1"), AccessConfigs: []*computepb.AccessConfig{ { - NatIP: ptr.To("203.0.113.1"), + NatIP: new("203.0.113.1"), }, }, Ipv6AccessConfigs: []*computepb.AccessConfig{ { - ExternalIpv6: ptr.To("2001:db8::2"), + ExternalIpv6: new("2001:db8::2"), }, }, }, }, Disks: []*computepb.AttachedDisk{ { - Source: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a/disks/test-disk"), + Source: new("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a/disks/test-disk"), DiskEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-disk"), + KmsKeyName: new("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-disk"), }, InitializeParams: &computepb.AttachedDiskInitializeParams{ - SourceImage: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/global/images/test-source-image"), - SourceSnapshot: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/global/snapshots/test-source-snapshot"), + SourceImage: new("https://www.googleapis.com/compute/v1/projects/test-project-id/global/images/test-source-image"), + SourceSnapshot: new("https://www.googleapis.com/compute/v1/projects/test-project-id/global/snapshots/test-source-snapshot"), SourceImageEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-image"), + KmsKeyName: new("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-image"), }, SourceSnapshotEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-snapshot"), + KmsKeyName: new("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-snapshot"), }, }, }, }, ServiceAccounts: []*computepb.ServiceAccount{ { - Email: ptr.To("test-sa@test-project-id.iam.gserviceaccount.com"), + Email: new("test-sa@test-project-id.iam.gserviceaccount.com"), }, }, GuestAccelerators: []*computepb.AcceleratorConfig{ { - AcceleratorType: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a/acceleratorTypes/nvidia-tesla-k80"), - AcceleratorCount: ptr.To[int32](1), + AcceleratorType: new("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a/acceleratorTypes/nvidia-tesla-k80"), + AcceleratorCount: new(int32(1)), }, }, }, MachineImageEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-machine-encryption-key"), + KmsKeyName: new("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-machine-encryption-key"), }, - SourceInstance: ptr.To("projects/test-project-id/zones/us-central1-a/instances/test-instance"), + SourceInstance: new("projects/test-project-id/zones/us-central1-a/instances/test-instance"), SavedDisks: []*computepb.SavedDisk{ { - SourceDisk: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a/disks/test-saved-disk"), + SourceDisk: new("https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a/disks/test-saved-disk"), }, }, } diff --git a/sources/gcp/manual/compute-node-group.go b/sources/gcp/manual/compute-node-group.go index cc6b6960..6e0b1253 100644 --- a/sources/gcp/manual/compute-node-group.go +++ b/sources/gcp/manual/compute-node-group.go @@ -8,8 +8,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "google.golang.org/protobuf/proto" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -192,7 +190,7 @@ func (c computeNodeGroupWrapper) listAggregatedStream(ctx context.Context, strea p.Go(func(ctx context.Context) error { it := c.client.AggregatedList(ctx, &computepb.AggregatedListNodeGroupsRequest{ Project: projectID, - ReturnPartialSuccess: proto.Bool(true), // Handle partial failures gracefully + ReturnPartialSuccess: new(true), // Handle partial failures gracefully }) for { @@ -274,7 +272,7 @@ func (c computeNodeGroupWrapper) SearchStream(ctx context.Context, stream discov req := &computepb.ListNodeGroupsRequest{ Project: location.ProjectID, Zone: location.Zone, - Filter: ptr.To("nodeTemplate = " + nodeTemplate), + Filter: new("nodeTemplate = " + nodeTemplate), } it := c.client.List(ctx, req) diff --git a/sources/gcp/manual/compute-node-group_test.go b/sources/gcp/manual/compute-node-group_test.go index e099d5f3..0860f085 100644 --- a/sources/gcp/manual/compute-node-group_test.go +++ b/sources/gcp/manual/compute-node-group_test.go @@ -10,7 +10,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -453,8 +452,8 @@ func TestComputeNodeGroup(t *testing.T) { func createComputeNodeGroup(name, templateUrl string, status computepb.NodeGroup_Status) *computepb.NodeGroup { return &computepb.NodeGroup{ - Name: ptr.To(name), - NodeTemplate: ptr.To(templateUrl), - Status: ptr.To(status.String()), + Name: new(name), + NodeTemplate: new(templateUrl), + Status: new(status.String()), } } diff --git a/sources/gcp/manual/compute-node-template.go b/sources/gcp/manual/compute-node-template.go index 34b40b17..4beea88f 100644 --- a/sources/gcp/manual/compute-node-template.go +++ b/sources/gcp/manual/compute-node-template.go @@ -8,7 +8,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "google.golang.org/protobuf/proto" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -176,7 +175,7 @@ func (c computeNodeTemplateWrapper) listAggregatedStream(ctx context.Context, st p.Go(func(ctx context.Context) error { it := c.client.AggregatedList(ctx, &computepb.AggregatedListNodeTemplatesRequest{ Project: projectID, - ReturnPartialSuccess: proto.Bool(true), // Handle partial failures gracefully + ReturnPartialSuccess: new(true), // Handle partial failures gracefully }) for { diff --git a/sources/gcp/manual/compute-node-template_test.go b/sources/gcp/manual/compute-node-template_test.go index e4299916..00a8fbcb 100644 --- a/sources/gcp/manual/compute-node-template_test.go +++ b/sources/gcp/manual/compute-node-template_test.go @@ -9,7 +9,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -266,12 +265,12 @@ func TestComputeNodeTemplate(t *testing.T) { // Create an node template fixture (as returned from GCP API). func createNodeTemplateApiFixture(nodeTemplateName string) *computepb.NodeTemplate { return &computepb.NodeTemplate{ - Name: ptr.To(nodeTemplateName), - NodeType: ptr.To("c2-node-60-240"), + Name: new(nodeTemplateName), + NodeType: new("c2-node-60-240"), ServerBinding: &computepb.ServerBinding{ - Type: ptr.To("RESTART_NODE_ON_ANY_SERVER"), + Type: new("RESTART_NODE_ON_ANY_SERVER"), }, - SelfLink: ptr.To("test-self-link"), - Region: ptr.To("us-central1"), + SelfLink: new("test-self-link"), + Region: new("us-central1"), } } 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 2e90abaa..4004adfc 100644 --- a/sources/gcp/manual/compute-region-instance-group-manager_test.go +++ b/sources/gcp/manual/compute-region-instance-group-manager_test.go @@ -8,7 +8,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -314,17 +313,17 @@ func TestComputeRegionInstanceGroupManager(t *testing.T) { func createRegionInstanceGroupManager(name string, isStable bool, instanceTemplate string) *computepb.InstanceGroupManager { return &computepb.InstanceGroupManager{ - Name: ptr.To(name), + Name: new(name), Status: &computepb.InstanceGroupManagerStatus{ - IsStable: ptr.To(isStable), - Autoscaler: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/autoscalers/test-autoscaler"), + IsStable: new(isStable), + Autoscaler: new("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/autoscalers/test-autoscaler"), }, - Region: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1"), - InstanceTemplate: ptr.To(instanceTemplate), - InstanceGroup: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/instanceGroups/test-group"), + Region: new("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1"), + InstanceTemplate: new(instanceTemplate), + InstanceGroup: new("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/instanceGroups/test-group"), TargetPools: []string{"https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/targetPools/test-pool"}, ResourcePolicies: &computepb.InstanceGroupManagerResourcePolicies{ - WorkloadPolicy: ptr.To("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/resourcePolicies/test-policy"), + WorkloadPolicy: new("https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/resourcePolicies/test-policy"), }, } } diff --git a/sources/gcp/manual/compute-reservation.go b/sources/gcp/manual/compute-reservation.go index b29260a8..cc541a54 100644 --- a/sources/gcp/manual/compute-reservation.go +++ b/sources/gcp/manual/compute-reservation.go @@ -8,7 +8,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/sourcegraph/conc/pool" "google.golang.org/api/iterator" - "google.golang.org/protobuf/proto" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -178,7 +177,7 @@ func (c computeReservationWrapper) listAggregatedStream(ctx context.Context, str p.Go(func(ctx context.Context) error { it := c.client.AggregatedList(ctx, &computepb.AggregatedListReservationsRequest{ Project: projectID, - ReturnPartialSuccess: proto.Bool(true), // Handle partial failures gracefully + ReturnPartialSuccess: new(true), // Handle partial failures gracefully }) for { diff --git a/sources/gcp/manual/compute-reservation_test.go b/sources/gcp/manual/compute-reservation_test.go index 9e317dbc..8cefce1b 100644 --- a/sources/gcp/manual/compute-reservation_test.go +++ b/sources/gcp/manual/compute-reservation_test.go @@ -9,7 +9,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -283,18 +282,18 @@ func TestComputeReservation(t *testing.T) { func createComputeReservation(reservationName string, status computepb.Reservation_Status) *computepb.Reservation { return &computepb.Reservation{ - Name: ptr.To(reservationName), - Commitment: ptr.To( + Name: new(reservationName), + Commitment: new( "https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/commitments/test-commitment", ), SpecificReservation: &computepb.AllocationSpecificSKUReservation{ InstanceProperties: &computepb.AllocationSpecificSKUAllocationReservedInstanceProperties{ - MachineType: ptr.To( + MachineType: new( "https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a/machineTypes/n1-standard-1", ), GuestAccelerators: []*computepb.AcceleratorConfig{ { - AcceleratorType: ptr.To( + AcceleratorType: new( "https://www.googleapis.com/compute/v1/projects/test-project-id/zones/us-central1-a/acceleratorTypes/nvidia-tesla-k80", ), }, @@ -304,6 +303,6 @@ func createComputeReservation(reservationName string, status computepb.Reservati ResourcePolicies: map[string]string{ "policy1": "https://www.googleapis.com/compute/v1/projects/test-project-id/regions/us-central1/resourcePolicies/test-policy", }, - Status: ptr.To(status.String()), + Status: new(status.String()), } } diff --git a/sources/gcp/manual/compute-security-policy_test.go b/sources/gcp/manual/compute-security-policy_test.go index b93d4db3..685aa79c 100644 --- a/sources/gcp/manual/compute-security-policy_test.go +++ b/sources/gcp/manual/compute-security-policy_test.go @@ -8,7 +8,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -199,13 +198,13 @@ func TestComputeSecurityPolicy(t *testing.T) { func createComputeSecurityPolicy(policyName string) *computepb.SecurityPolicy { return &computepb.SecurityPolicy{ - Name: ptr.To(policyName), + Name: new(policyName), Labels: map[string]string{"env": "test"}, Rules: []*computepb.SecurityPolicyRule{ { - Priority: ptr.To(int32(1000)), + Priority: new(int32(1000)), }, }, - Region: ptr.To("us-central1"), + Region: new("us-central1"), } } diff --git a/sources/gcp/manual/compute-snapshot_test.go b/sources/gcp/manual/compute-snapshot_test.go index 34b013f8..9856915c 100644 --- a/sources/gcp/manual/compute-snapshot_test.go +++ b/sources/gcp/manual/compute-snapshot_test.go @@ -8,7 +8,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "go.uber.org/mock/gomock" "google.golang.org/api/iterator" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -77,10 +76,10 @@ func TestComputeSnapshot(t *testing.T) { ExpectedScope: "test-project-id", }, { - ExpectedType: gcpshared.ComputeResourcePolicy.String(), - ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-source-snapshot-schedule-policy", - ExpectedScope: "test-project-id.us-central1", + ExpectedType: gcpshared.ComputeResourcePolicy.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-source-snapshot-schedule-policy", + ExpectedScope: "test-project-id.us-central1", }, { ExpectedType: gcpshared.CloudKMSCryptoKeyVersion.String(), @@ -295,23 +294,23 @@ func TestComputeSnapshot(t *testing.T) { func createComputeSnapshot(snapshotName string, status computepb.Snapshot_Status) *computepb.Snapshot { return &computepb.Snapshot{ - Name: ptr.To(snapshotName), + Name: new(snapshotName), Labels: map[string]string{"env": "test"}, - Status: ptr.To(status.String()), - SourceInstantSnapshot: ptr.To("projects/test-project-id/zones/us-central1-a/instantSnapshots/test-instant-snapshot"), + Status: new(status.String()), + SourceInstantSnapshot: new("projects/test-project-id/zones/us-central1-a/instantSnapshots/test-instant-snapshot"), StorageLocations: []string{"us-central1"}, Licenses: []string{"projects/test-project-id/global/licenses/test-license"}, SourceDiskEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-disk"), + KmsKeyName: new("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-disk"), }, - SourceDisk: ptr.To("projects/test-project-id/zones/us-central1-a/disks/test-disk"), + SourceDisk: new("projects/test-project-id/zones/us-central1-a/disks/test-disk"), SourceInstantSnapshotEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-snapshot"), - RawKey: ptr.To("test-key"), + KmsKeyName: new("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-source-snapshot"), + RawKey: new("test-key"), }, - SourceSnapshotSchedulePolicy: ptr.To("projects/test-project-id/regions/us-central1/resourcePolicies/test-source-snapshot-schedule-policy"), + SourceSnapshotSchedulePolicy: new("projects/test-project-id/regions/us-central1/resourcePolicies/test-source-snapshot-schedule-policy"), SnapshotEncryptionKey: &computepb.CustomerEncryptionKey{ - KmsKeyName: ptr.To("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-snapshot"), + KmsKeyName: new("projects/test-project-id/locations/global/keyRings/test-keyring/cryptoKeys/test-key/cryptoKeyVersions/test-version-snapshot"), }, } } diff --git a/sources/gcp/manual/storage-bucket-iam-policy.go b/sources/gcp/manual/storage-bucket-iam-policy.go index 3ce013d0..b98fcc2b 100644 --- a/sources/gcp/manual/storage-bucket-iam-policy.go +++ b/sources/gcp/manual/storage-bucket-iam-policy.go @@ -158,7 +158,7 @@ func (w *storageBucketIAMPolicyWrapper) policyToItem(location gcpshared.Location } type policyAttrs struct { - Bucket string `json:"bucket"` + Bucket string `json:"bucket"` Bindings []policyBinding `json:"bindings"` } attrs, err := shared.ToAttributesWithExclude(policyAttrs{Bucket: bucketName, Bindings: policyBindings}) @@ -274,12 +274,12 @@ func extractCustomRoleProjectAndID(role string) (projectID, roleID string) { return "", "" } rest := strings.TrimPrefix(role, prefix) - idx := strings.Index(rest, suffix) - if idx == -1 { + before, after, ok := strings.Cut(rest, suffix) + if !ok { return "", "" } - projectID = rest[:idx] - roleID = rest[idx+len(suffix):] + projectID = before + roleID = after if projectID == "" || roleID == "" { return "", "" } @@ -291,10 +291,10 @@ func extractCustomRoleProjectAndID(role string) (projectID, roleID string) { // 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:") + if after, ok := strings.CutPrefix(member, "deleted:domain:"); ok { + domain = after + } else if after, ok := strings.CutPrefix(member, "domain:"); ok { + domain = after } else { return "" } @@ -309,8 +309,8 @@ func extractDomainFromDomainMember(member string) string { // (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) + if after, ok := strings.CutPrefix(member, prefix); ok { + return after } } return "" @@ -320,10 +320,10 @@ func extractProjectIDFromProjectPrincipalMember(member string) string { // 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:") + if after, ok := strings.CutPrefix(member, "deleted:serviceAccount:"); ok { + email = after + } else if after, ok := strings.CutPrefix(member, "serviceAccount:"); ok { + email = after } else { return "" } @@ -339,21 +339,21 @@ func extractServiceAccountEmailFromMember(member string) string { // 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 { + _, after, ok := strings.Cut(email, "@") + if !ok { return "" } - domain := email[at+1:] + domain := after // 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 { + before, _, ok := strings.Cut(domain, ".") + if !ok { return "" } - return domain[:dot] + return before } // isGoogleManagedServiceAccountDomain reports whether the domain's first label is a known diff --git a/sources/gcp/proc/proc.go b/sources/gcp/proc/proc.go index 588f8e3d..d59b1ff6 100644 --- a/sources/gcp/proc/proc.go +++ b/sources/gcp/proc/proc.go @@ -59,13 +59,13 @@ func (r *ProjectPermissionCheckResult) FormatError() error { // Build error message var errMsg strings.Builder - errMsg.WriteString(fmt.Sprintf("%d out of %d projects (%.1f%%) failed permission checks\n\n", - r.FailureCount, totalProjects, failurePercentage)) + fmt.Fprintf(&errMsg, "%d out of %d projects (%.1f%%) failed permission checks\n\n", + r.FailureCount, totalProjects, failurePercentage) // List failed projects with their errors errMsg.WriteString("Failed projects:\n") for projectID, err := range r.ProjectErrors { - errMsg.WriteString(fmt.Sprintf(" - %s: %v\n", projectID, err)) + fmt.Fprintf(&errMsg, " - %s: %v\n", projectID, err) } return errors.New(errMsg.String()) diff --git a/sources/gcp/proc/proc_test.go b/sources/gcp/proc/proc_test.go index 5089058b..c5476e72 100644 --- a/sources/gcp/proc/proc_test.go +++ b/sources/gcp/proc/proc_test.go @@ -3,6 +3,7 @@ package proc import ( "context" "fmt" + "slices" "sort" "strings" "sync" @@ -113,11 +114,8 @@ func Test_ensureMandatoryFieldsInDynamicAdapters(t *testing.T) { foundPerm := false for _, perm := range role.IAMPermissions { - for _, iamPerm := range meta.IAMPermissions { - if perm == iamPerm { - foundPerm = true - break - } + if slices.Contains(meta.IAMPermissions, perm) { + foundPerm = true } } @@ -580,14 +578,12 @@ func TestProjectHealthChecker_Check_ConcurrentAccess(t *testing.T) { errors := make(chan error, concurrency) for range concurrency { - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { _, err := checker.Check(ctx) if err != nil { errors <- err } - }() + }) } wg.Wait() @@ -717,11 +713,11 @@ func TestCriticalTerraformMappingsRegistered(t *testing.T) { // 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 + terraformType string + expectedType string + expectedField string + expectedMethod sdp.QueryMethod + reason string // documents why this mapping is critical }{ // Core resource mappings { diff --git a/sources/gcp/shared/base.go b/sources/gcp/shared/base.go index 57e46afa..e15f2037 100644 --- a/sources/gcp/shared/base.go +++ b/sources/gcp/shared/base.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -80,10 +81,8 @@ func (z *ZoneBase) LocationFromScope(scope string) (LocationInfo, error) { if !location.Zonal() { return LocationInfo{}, fmt.Errorf("scope %s is not zonal", scope) } - for _, loc := range z.locations { - if location.Equals(loc) { - return location, nil - } + if slices.ContainsFunc(z.locations, location.Equals) { + return location, nil } return LocationInfo{}, fmt.Errorf("scope %s not found in adapter locations", scope) } @@ -142,10 +141,8 @@ func (r *RegionBase) LocationFromScope(scope string) (LocationInfo, error) { if !location.Regional() { return LocationInfo{}, fmt.Errorf("scope %s is not regional", scope) } - for _, loc := range r.locations { - if location.Equals(loc) { - return location, nil - } + if slices.ContainsFunc(r.locations, location.Equals) { + return location, nil } return LocationInfo{}, fmt.Errorf("scope %s not found in adapter locations", scope) } @@ -203,10 +200,8 @@ func (p *ProjectBase) LocationFromScope(scope string) (LocationInfo, error) { if !location.ProjectLevel() { return LocationInfo{}, fmt.Errorf("scope %s is not project-level", scope) } - for _, loc := range p.locations { - if location.Equals(loc) { - return location, nil - } + if slices.ContainsFunc(p.locations, location.Equals) { + return location, nil } return LocationInfo{}, fmt.Errorf("scope %s not found in adapter locations", scope) } diff --git a/sources/gcp/shared/kms-asset-loader.go b/sources/gcp/shared/kms-asset-loader.go index 93c481cb..2bae3e7f 100644 --- a/sources/gcp/shared/kms-asset-loader.go +++ b/sources/gcp/shared/kms-asset-loader.go @@ -66,7 +66,7 @@ func (l *CloudKMSAssetLoader) EnsureLoaded(ctx context.Context) error { // Use singleflight to ensure only one load runs at a time // Concurrent callers wait for the same result - _, err, _ := l.group.Do("load", func() (interface{}, error) { + _, err, _ := l.group.Do("load", func() (any, error) { // Double-check TTL after acquiring the flight l.mu.Lock() if time.Since(l.lastLoadTime) < shared.DefaultCacheDuration { @@ -231,7 +231,7 @@ func (l *CloudKMSAssetLoader) fetchAssetsPage(ctx context.Context, pageToken str // Cloud Asset API requires quota project header req.Header.Set("X-Goog-User-Project", l.projectID) - resp, err := l.httpClient.Do(req) + resp, err := l.httpClient.Do(req) //nolint:gosec // G107 (SSRF): URL built from hardcoded https://cloudasset.googleapis.com/v1 base with project ID if err != nil { return nil, "", fmt.Errorf("failed to execute request: %w", err) } diff --git a/sources/gcp/shared/location_info.go b/sources/gcp/shared/location_info.go index 4a5c65e1..e362f65b 100644 --- a/sources/gcp/shared/location_info.go +++ b/sources/gcp/shared/location_info.go @@ -2,6 +2,7 @@ package shared import ( "fmt" + "slices" "strings" ) @@ -226,10 +227,8 @@ func GetProjectIDsFromLocations(locationSlices ...[]LocationInfo) []string { // (e.g., filtering aggregatedList results to only configured locations). func HasLocationInSlices(loc LocationInfo, locationSlices ...[]LocationInfo) bool { for _, locations := range locationSlices { - for _, configuredLoc := range locations { - if loc.Equals(configuredLoc) { - return true - } + if slices.ContainsFunc(locations, loc.Equals) { + return true } } return false diff --git a/sources/gcp/shared/manual-adapter-links.go b/sources/gcp/shared/manual-adapter-links.go index d3004001..612d5acf 100644 --- a/sources/gcp/shared/manual-adapter-links.go +++ b/sources/gcp/shared/manual-adapter-links.go @@ -571,7 +571,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: projectIDFromName, Scope: projectIDFromName, // Project scope uses project ID as scope }, - } + } } } else if strings.HasPrefix(name, "folders/") { folderID := ExtractPathParam("folders", name) @@ -583,7 +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) }, - } + } } } else if strings.HasPrefix(name, "organizations/") { orgID := ExtractPathParam("organizations", name) @@ -595,7 +595,7 @@ 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) }, - } + } } } return nil @@ -615,7 +615,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) }, - } + } } } return nil @@ -635,7 +635,7 @@ 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) }, - } + } } } return nil @@ -685,7 +685,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: httpURL, Scope: "global", }, - } + } } } return nil @@ -737,7 +737,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(values[1], values[2]), Scope: values[0], }, - } + } } } @@ -752,7 +752,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(values[1], values[2]), Scope: values[0], }, - } + } } } @@ -807,7 +807,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: values[1], Scope: values[0], }, - } + } } } @@ -883,7 +883,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: dataset, Scope: scope, }, - } + } } } @@ -898,7 +898,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: parts[1], // dataset ID Scope: parts[0], // project ID }, - } + } } } @@ -942,7 +942,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(dataset, model), Scope: scope, }, - } + } } } @@ -959,7 +959,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: shared.CompositeLookupKey(dataset, model), Scope: scope, }, - } + } } } @@ -987,7 +987,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: values[1], Scope: values[0], }, - } + } } } @@ -1002,7 +1002,7 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem Query: values[1], Scope: values[0], }, - } + } } } @@ -1011,8 +1011,8 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem // Extract bucket name (everything before the first slash) bucketName := query - if idx := strings.Index(query, "/"); idx != -1 { - bucketName = query[:idx] + if before, _, ok := strings.Cut(query, "/"); ok { + bucketName = before } // Validate bucket name is not empty @@ -1037,8 +1037,8 @@ var ManualAdapterLinksByAssetType = map[shared.ItemType]func(projectID, fromItem // 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 before, _, ok := strings.Cut(query, "/"); ok { + bucketName = before } if projectID == "" || bucketName == "" { return nil diff --git a/sources/shared/util.go b/sources/shared/util.go index 4f04fa51..aa4b98df 100644 --- a/sources/shared/util.go +++ b/sources/shared/util.go @@ -9,7 +9,7 @@ import ( // ToAttributesWithExclude converts an interface to SDP attributes using the `sdp.ToAttributesSorted` // function, and also allows the user to exclude certain top-level fields from // the resulting attributes -func ToAttributesWithExclude(i interface{}, exclusions ...string) (*sdp.ItemAttributes, error) { +func ToAttributesWithExclude(i any, exclusions ...string) (*sdp.ItemAttributes, error) { attrs, err := sdp.ToAttributesViaJson(i) if err != nil { return nil, err diff --git a/sources/snapshot/adapters/index_test.go b/sources/snapshot/adapters/index_test.go index 0cb4a6ce..a5774e65 100644 --- a/sources/snapshot/adapters/index_test.go +++ b/sources/snapshot/adapters/index_test.go @@ -7,15 +7,15 @@ import ( ) func createTestSnapshot() *sdp.Snapshot { - attrs1, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs1, _ := sdp.ToAttributesViaJson(map[string]any{ "instanceId": "i-12345", "name": "test-instance", }) - attrs2, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs2, _ := sdp.ToAttributesViaJson(map[string]any{ "instanceId": "i-67890", "name": "test-instance-2", }) - attrs3, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs3, _ := sdp.ToAttributesViaJson(map[string]any{ "bucketName": "my-test-bucket", }) diff --git a/sources/snapshot/adapters/loader.go b/sources/snapshot/adapters/loader.go index d0dcb96a..8f702285 100644 --- a/sources/snapshot/adapters/loader.go +++ b/sources/snapshot/adapters/loader.go @@ -60,7 +60,7 @@ func loadSnapshotFromURL(ctx context.Context, url string) ([]byte, error) { } client := &http.Client{} - resp, err := client.Do(req) + resp, err := client.Do(req) //nolint:gosec // G107 (SSRF): URL comes from operator-supplied snapshot source config, not from untrusted network input if err != nil { return nil, fmt.Errorf("HTTP request failed: %w", err) } diff --git a/sources/snapshot/adapters/loader_test.go b/sources/snapshot/adapters/loader_test.go index 7ae5d63b..ff8c5053 100644 --- a/sources/snapshot/adapters/loader_test.go +++ b/sources/snapshot/adapters/loader_test.go @@ -14,7 +14,7 @@ import ( func TestLoadSnapshotFromFile(t *testing.T) { // Create a test snapshot - attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "test-item", }) @@ -63,7 +63,7 @@ func TestLoadSnapshotFromFile(t *testing.T) { func TestLoadSnapshotFromURL(t *testing.T) { // Create a test snapshot - attrs, _ := sdp.ToAttributesViaJson(map[string]interface{}{ + attrs, _ := sdp.ToAttributesViaJson(map[string]any{ "name": "test-item", }) diff --git a/sources/transformer.go b/sources/transformer.go index d7617c28..d114d7cd 100644 --- a/sources/transformer.go +++ b/sources/transformer.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "slices" "strings" "buf.build/go/protovalidate" @@ -241,10 +242,8 @@ func (s *standardAdapterCore) validateScopes(scope string) error { } } - for _, expectedScope := range s.Scopes() { - if scope == expectedScope { - return nil - } + if slices.Contains(s.Scopes(), scope) { + return nil } return &sdp.QueryError{ @@ -1088,13 +1087,7 @@ func validatePredefinedRole(wrapper Wrapper) error { // Check if all IAM permissions from the wrapper exist in the predefined role's IAMPermissions for _, perm := range iamPermissions { - found := false - for _, rolePerm := range role.IAMPermissions { - if perm == rolePerm { - found = true - break - } - } + found := slices.Contains(role.IAMPermissions, perm) if !found { return fmt.Errorf("IAM permission %s from wrapper is not included in predefined role %s IAMPermissions", perm, pRole) } diff --git a/sources/transformer_test.go b/sources/transformer_test.go index ac18c762..c78a2730 100644 --- a/sources/transformer_test.go +++ b/sources/transformer_test.go @@ -150,24 +150,20 @@ func TestListErrorCausesCacheHang(t *testing.T) { var secondDuration time.Duration // First goroutine: calls List(), gets cache miss, underlying returns error - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { start := time.Now() _, firstErr = adapter.(interface { List(context.Context, string, bool) ([]*sdp.Item, error) }).List(ctx, scope, false) firstDuration = time.Since(start) - }() + }) // Give first goroutine time to start and hit the error time.Sleep(50 * time.Millisecond) // Second goroutine: calls List() after first has hit error // Should be woken immediately by done() and retry quickly - wg.Add(1) - go func() { - defer wg.Done() + wg.Go(func() { // Use a timeout to prevent infinite hang if bug exists ctx2, cancel := context.WithTimeout(ctx, 500*time.Millisecond) defer cancel() @@ -177,7 +173,7 @@ func TestListErrorCausesCacheHang(t *testing.T) { List(context.Context, string, bool) ([]*sdp.Item, error) }).List(ctx2, scope, false) secondDuration = time.Since(start) - }() + }) wg.Wait() diff --git a/stdlib-source/adapters/certificate.go b/stdlib-source/adapters/certificate.go index 018c2c5e..6fd6e67a 100644 --- a/stdlib-source/adapters/certificate.go +++ b/stdlib-source/adapters/certificate.go @@ -135,7 +135,7 @@ func (s *CertificateAdapter) Search(ctx context.Context, scope string, query str continue } - attributes, err = sdp.ToAttributes(map[string]interface{}{ + attributes, err = sdp.ToAttributes(map[string]any{ "issuer": cert.Issuer.String(), "subject": cert.Subject.String(), "notBefore": cert.NotBefore.String(), @@ -149,7 +149,7 @@ func (s *CertificateAdapter) Search(ctx context.Context, scope string, query str "keyUsage": getKeyUsage(cert.KeyUsage), "extendedKeyUsage": getExtendedKeyUsage(cert.ExtKeyUsage), "version": cert.Version, - "basicConstraints": map[string]interface{}{ + "basicConstraints": map[string]any{ "CA": cert.IsCA, "pathLen": cert.MaxPathLen, }, diff --git a/stdlib-source/adapters/certificate_test.go b/stdlib-source/adapters/certificate_test.go index 871fda7c..05580e0e 100644 --- a/stdlib-source/adapters/certificate_test.go +++ b/stdlib-source/adapters/certificate_test.go @@ -98,7 +98,7 @@ func TestCertificateList(t *testing.T) { type CertTest struct { Attribute string - Expected interface{} + Expected any } func (c *CertTest) Run(t *testing.T, cert *sdp.Item) { @@ -185,7 +185,7 @@ func TestCertificateSearch(t *testing.T) { }, { Attribute: "basicConstraints", - Expected: map[string]interface{}{ + Expected: map[string]any{ "pathLen": float64(0), "CA": true, }, diff --git a/stdlib-source/adapters/dns.go b/stdlib-source/adapters/dns.go index 3d7879fe..dfbe264a 100644 --- a/stdlib-source/adapters/dns.go +++ b/stdlib-source/adapters/dns.go @@ -472,7 +472,7 @@ func (d *DNSAdapter) makeQueryImpl(ctx context.Context, query string, server str name := trimDnsSuffix(cname.Hdr.Name) target := trimDnsSuffix(cname.Target) - attrs, err = sdp.ToAttributes(map[string]interface{}{ + attrs, err = sdp.ToAttributes(map[string]any{ "name": name, "type": "CNAME", "ttl": cname.Hdr.Ttl, @@ -569,7 +569,7 @@ func GroupAnswers(answers []dns.RR) *AnswerGroup { // AToItem Converts a set of A or AAAA records to an item func AToItem(name string, records []dns.RR) (*sdp.Item, error) { - recordAttrs := make([]map[string]interface{}, 0) + recordAttrs := make([]map[string]any, 0) liq := make([]*sdp.LinkedItemQuery, 0) for _, r := range records { @@ -585,7 +585,7 @@ func AToItem(name string, records []dns.RR) (*sdp.Item, error) { ip = aaaa.AAAA } - recordAttrs = append(recordAttrs, map[string]interface{}{ + recordAttrs = append(recordAttrs, map[string]any{ "ttl": hdr.Ttl, "type": typ, "ip": ip.String(), @@ -607,7 +607,7 @@ func AToItem(name string, records []dns.RR) (*sdp.Item, error) { return fmt.Sprint(i) < fmt.Sprint(j) }) - attrs, err := sdp.ToAttributes(map[string]interface{}{ + attrs, err := sdp.ToAttributes(map[string]any{ "name": name, "type": "address", "records": recordAttrs, diff --git a/stdlib-source/adapters/http.go b/stdlib-source/adapters/http.go index 4db87391..5b593dd7 100644 --- a/stdlib-source/adapters/http.go +++ b/stdlib-source/adapters/http.go @@ -187,7 +187,7 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor // we are only running a HEAD request this is unlikely to be a problem tr := &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, //nolint:gosec // This is fine for a HEAD request + InsecureSkipVerify: true, //nolint:gosec // G402 (TLS skip verify): intentional—adapter inspects TLS certificate details via HEAD request, not trusting the content }, } client := &http.Client{ @@ -214,7 +214,7 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor var res *http.Response - res, err = client.Do(req) + res, err = client.Do(req) //nolint:gosec // G107 (SSRF): URL is the SDP query target; hostname validated by validateHostname() which blocks link-local/metadata IPs if err != nil { err = &sdp.QueryError{ @@ -254,7 +254,7 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor // Convert the attributes from a golang map, to the structure required for // the SDP protocol - attributes, err := sdp.ToAttributes(map[string]interface{}{ + attributes, err := sdp.ToAttributes(map[string]any{ "name": query, "status": res.StatusCode, "statusString": res.Status, @@ -319,7 +319,7 @@ func (s *HTTPAdapter) Get(ctx context.Context, scope string, query string, ignor version = "unknown" } - attributes.Set("tls", map[string]interface{}{ + attributes.Set("tls", map[string]any{ "version": version, "certificate": CertToName(tlsState.PeerCertificates[0]), "serverName": tlsState.ServerName, diff --git a/stdlib-source/adapters/http_test.go b/stdlib-source/adapters/http_test.go index 5370cd79..6ecc61d2 100644 --- a/stdlib-source/adapters/http_test.go +++ b/stdlib-source/adapters/http_test.go @@ -306,7 +306,7 @@ func TestHTTPGet(t *testing.T) { t.Fatal(err) } - var status interface{} + var status any status, err = item.GetAttributes().Get("status") if err != nil { @@ -326,7 +326,7 @@ func TestHTTPGet(t *testing.T) { t.Fatal(err) } - var status interface{} + var status any status, err = item.GetAttributes().Get("status") if err != nil { @@ -360,7 +360,7 @@ func TestHTTPGet(t *testing.T) { t.Fatal(err) } - var status interface{} + var status any status, err = item.GetAttributes().Get("status") if err != nil { t.Fatal(err) @@ -371,7 +371,7 @@ func TestHTTPGet(t *testing.T) { } // Check that the location header contains the relative URL - var location interface{} + var location any location, err = item.GetAttributes().Get("location") if err != nil { t.Fatal(err) @@ -502,7 +502,7 @@ func TestHTTPGet(t *testing.T) { } // The request should succeed, but the redirect should be marked as blocked - var locationError interface{} + var locationError any locationError, err = item.GetAttributes().Get("location-error") if err != nil { t.Fatal("Expected location-error attribute for blocked redirect") @@ -560,7 +560,7 @@ func TestHTTPSearch(t *testing.T) { } // Verify the item has the expected status (200 for OK page) - var status interface{} + var status any status, err = item.GetAttributes().Get("status") if err != nil { t.Fatal(err) diff --git a/stdlib-source/adapters/ip.go b/stdlib-source/adapters/ip.go index dfc1d419..e24816cc 100644 --- a/stdlib-source/adapters/ip.go +++ b/stdlib-source/adapters/ip.go @@ -127,7 +127,7 @@ func (bc *IPAdapter) Get(ctx context.Context, scope string, query string, ignore } } - attributes, err = sdp.ToAttributes(map[string]interface{}{ + attributes, err = sdp.ToAttributes(map[string]any{ "ip": ip.String(), "unspecified": ip.IsUnspecified(), "loopback": ip.IsLoopback(), diff --git a/stdlib-source/adapters/main.go b/stdlib-source/adapters/main.go index 1c8ddfa0..c69dbb8c 100644 --- a/stdlib-source/adapters/main.go +++ b/stdlib-source/adapters/main.go @@ -171,7 +171,7 @@ func parseRdapUrl(rdapUrl string) (*RDAPUrl, error) { } var RDAPTransforms = sdp.AddDefaultTransforms(sdp.TransformMap{ - reflect.TypeOf(rdap.Link{}): func(i interface{}) interface{} { + reflect.TypeFor[rdap.Link](): func(i any) any { // We only want to return the href for links link, ok := i.(rdap.Link) @@ -181,7 +181,7 @@ var RDAPTransforms = sdp.AddDefaultTransforms(sdp.TransformMap{ return "" }, - reflect.TypeOf(rdap.VCard{}): func(i interface{}) interface{} { + reflect.TypeFor[rdap.VCard](): func(i any) any { vcard, ok := i.(rdap.VCard) if ok { @@ -230,7 +230,7 @@ var RDAPTransforms = sdp.AddDefaultTransforms(sdp.TransformMap{ return nil }, - reflect.TypeOf(&rdap.DecodeData{}): func(i interface{}) interface{} { + reflect.TypeFor[*rdap.DecodeData](): func(i any) any { // Exclude these return nil }, diff --git a/stdlib-source/adapters/rdap-asn.go b/stdlib-source/adapters/rdap-asn.go index dd41e7b9..41e7ae4f 100644 --- a/stdlib-source/adapters/rdap-asn.go +++ b/stdlib-source/adapters/rdap-asn.go @@ -99,7 +99,7 @@ func (s *RdapASNAdapter) Get(ctx context.Context, scope string, query string, ig return nil, fmt.Errorf("Unexpected response type: %T", response.Object) } - attributes, err := sdp.ToAttributesCustom(map[string]interface{}{ + attributes, err := sdp.ToAttributesCustom(map[string]any{ "conformance": asn.Conformance, "objectClassName": asn.ObjectClassName, "notices": asn.Notices, diff --git a/stdlib-source/adapters/rdap-domain.go b/stdlib-source/adapters/rdap-domain.go index 1502e069..afe5e15a 100644 --- a/stdlib-source/adapters/rdap-domain.go +++ b/stdlib-source/adapters/rdap-domain.go @@ -145,7 +145,7 @@ func (s *RdapDomainAdapter) Search(ctx context.Context, scope string, query stri } } - attributes, err := sdp.ToAttributesCustom(map[string]interface{}{ + attributes, err := sdp.ToAttributesCustom(map[string]any{ "conformance": domain.Conformance, "events": domain.Events, "handle": domain.Handle, diff --git a/stdlib-source/adapters/rdap-entity.go b/stdlib-source/adapters/rdap-entity.go index 9cb30028..1a462717 100644 --- a/stdlib-source/adapters/rdap-entity.go +++ b/stdlib-source/adapters/rdap-entity.go @@ -146,7 +146,7 @@ func (s *RdapEntityAdapter) runEntityRequest(ctx context.Context, query string, return nil, fmt.Errorf("Expected Entity, got %T", response.Object) } - attributes, err := sdp.ToAttributesCustom(map[string]interface{}{ + attributes, err := sdp.ToAttributesCustom(map[string]any{ "asEventActor": entity.AsEventActor, "conformance": entity.Conformance, "events": entity.Events, diff --git a/stdlib-source/adapters/rdap-ip-network.go b/stdlib-source/adapters/rdap-ip-network.go index 212b55a8..5b58dc7c 100644 --- a/stdlib-source/adapters/rdap-ip-network.go +++ b/stdlib-source/adapters/rdap-ip-network.go @@ -155,7 +155,7 @@ func (s *RdapIPNetworkAdapter) Search(ctx context.Context, scope string, query s s.IPCache.Store(network, ipNetwork, RdapCacheDuration) } - attributes, err := sdp.ToAttributesCustom(map[string]interface{}{ + attributes, err := sdp.ToAttributesCustom(map[string]any{ "conformance": ipNetwork.Conformance, "country": ipNetwork.Country, "endAddress": ipNetwork.EndAddress, diff --git a/stdlib-source/adapters/rdap-nameserver.go b/stdlib-source/adapters/rdap-nameserver.go index 0d0f052f..31e14e5a 100644 --- a/stdlib-source/adapters/rdap-nameserver.go +++ b/stdlib-source/adapters/rdap-nameserver.go @@ -141,7 +141,7 @@ func (s *RdapNameserverAdapter) Search(ctx context.Context, scope string, query return nil, fmt.Errorf("Expected Nameserver, got %T", response.Object) } - attributes, err := sdp.ToAttributesCustom(map[string]interface{}{ + attributes, err := sdp.ToAttributesCustom(map[string]any{ "conformance": nameserver.Conformance, "objectClassName": nameserver.ObjectClassName, "notices": nameserver.Notices, diff --git a/tfutils/aws_config.go b/tfutils/aws_config.go index bb957134..3baad19b 100644 --- a/tfutils/aws_config.go +++ b/tfutils/aws_config.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "maps" "net/http" "net/url" "os" @@ -56,7 +57,7 @@ type ProviderFile struct { type AWSProvider struct { Name string `hcl:"name,label" yaml:"name,omitempty"` Alias string `hcl:"alias,optional" yaml:"alias,omitempty"` - AccessKey string `hcl:"access_key,optional" yaml:"access_key,omitempty"` + AccessKey string `hcl:"access_key,optional" yaml:"access_key,omitempty"` //nolint:gosec // G101: field name, not a hardcoded credential; deserialized from local Terraform HCL config, never marshaled into logs or HTTP responses SecretKey string `hcl:"secret_key,optional" yaml:"secret_key,omitempty"` Token string `hcl:"token,optional" yaml:"token,omitempty"` Region string `hcl:"region,optional" yaml:"region,omitempty"` @@ -117,7 +118,7 @@ type AssumeRoleWithWebIdentity struct { // restore the default value to a cty value after tfconfig has // passed it through JSON to "void the caller needing to deal with // cty" -func ctyFromTfconfig(v interface{}) cty.Value { +func ctyFromTfconfig(v any) cty.Value { switch def := v.(type) { case bool: return cty.BoolVal(def) @@ -127,13 +128,13 @@ func ctyFromTfconfig(v interface{}) cty.Value { return cty.NumberIntVal(int64(def)) case string: return cty.StringVal(def) - case []interface{}: + case []any: d := make([]cty.Value, 0, len(def)) for _, v := range def { d = append(d, ctyFromTfconfig(v)) } return cty.ListVal(d) - case map[string]interface{}: + case map[string]any: d := map[string]cty.Value{} for k, v := range def { d[k] = ctyFromTfconfig(v) @@ -313,9 +314,7 @@ func setVariables(dest *hcl.EvalContext, variables map[string]cty.Value) { if variablesDest == nil { variablesDest = map[string]cty.Value{} } - for k, v := range variables { - variablesDest[k] = v - } + maps.Copy(variablesDest, variables) dest.Variables["var"] = cty.ObjectVal(variablesDest) } diff --git a/tfutils/azure_config.go b/tfutils/azure_config.go index c10af272..81f41ed7 100644 --- a/tfutils/azure_config.go +++ b/tfutils/azure_config.go @@ -17,7 +17,7 @@ type AzureProvider struct { SubscriptionID string `hcl:"subscription_id,optional" yaml:"subscription_id,omitempty"` TenantID string `hcl:"tenant_id,optional" yaml:"tenant_id,omitempty"` ClientID string `hcl:"client_id,optional" yaml:"client_id,omitempty"` - ClientSecret string `hcl:"client_secret,optional" yaml:"client_secret,omitempty"` + ClientSecret string `hcl:"client_secret,optional" yaml:"client_secret,omitempty"` //nolint:gosec // G101: field name, not a hardcoded credential; deserialized from local Terraform HCL config, never marshaled into logs or HTTP responses Environment string `hcl:"environment,optional" yaml:"environment,omitempty"` // Throw any additional stuff into here so it doesn't fail diff --git a/tfutils/gcp_config.go b/tfutils/gcp_config.go index 68853ebf..a82eda01 100644 --- a/tfutils/gcp_config.go +++ b/tfutils/gcp_config.go @@ -15,7 +15,7 @@ type GCPProvider struct { Name string `hcl:"name,label" yaml:"name,omitempty"` Alias string `hcl:"alias,optional" yaml:"alias,omitempty"` Credentials string `hcl:"credentials,optional" yaml:"credentials,omitempty"` - AccessToken string `hcl:"access_token,optional" yaml:"access_token,omitempty"` + AccessToken string `hcl:"access_token,optional" yaml:"access_token,omitempty"` //nolint:gosec // G101: field name, not a hardcoded credential; deserialized from local Terraform HCL config, never marshaled into logs or HTTP responses ImpersonateServiceAccount string `hcl:"impersonate_service_account,optional" yaml:"impersonate_service_account,omitempty"` Project string `hcl:"project,optional" yaml:"project,omitempty"` Region string `hcl:"region,optional" yaml:"region,omitempty"` diff --git a/tfutils/plan.go b/tfutils/plan.go index 19624902..574d4234 100644 --- a/tfutils/plan.go +++ b/tfutils/plan.go @@ -18,14 +18,14 @@ type Plan struct { FormatVersion string `json:"format_version,omitempty"` TerraformVersion string `json:"terraform_version,omitempty"` Variables Variables `json:"variables,omitempty"` - PlannedValues StateValues `json:"planned_values,omitempty"` + PlannedValues StateValues `json:"planned_values"` // ResourceDrift and ResourceChanges are sorted in a user-friendly order // that is undefined at this time, but consistent. ResourceDrift []ResourceChange `json:"resource_drift,omitempty"` ResourceChanges []ResourceChange `json:"resource_changes,omitempty"` OutputChanges map[string]Change `json:"output_changes,omitempty"` - PriorState State `json:"prior_state,omitempty"` - Config planConfig `json:"configuration,omitempty"` + PriorState State `json:"prior_state"` + Config planConfig `json:"configuration"` RelevantAttributes []ResourceAttr `json:"relevant_attributes,omitempty"` Checks json.RawMessage `json:"checks,omitempty"` Timestamp string `json:"timestamp,omitempty"` @@ -35,7 +35,7 @@ type Plan struct { // Config represents the complete configuration source type planConfig struct { ProviderConfigs map[string]ProviderConfig `json:"provider_config,omitempty"` - RootModule ConfigModule `json:"root_module,omitempty"` + RootModule ConfigModule `json:"root_module"` } // ProviderConfig describes all of the provider configurations throughout the @@ -43,12 +43,12 @@ type planConfig struct { // provider configurations are the one concept in Terraform that can span across // module boundaries. type ProviderConfig struct { - Name string `json:"name,omitempty"` - FullName string `json:"full_name,omitempty"` - Alias string `json:"alias,omitempty"` - VersionConstraint string `json:"version_constraint,omitempty"` - ModuleAddress string `json:"module_address,omitempty"` - Expressions map[string]interface{} `json:"expressions,omitempty"` + Name string `json:"name,omitempty"` + FullName string `json:"full_name,omitempty"` + Alias string `json:"alias,omitempty"` + VersionConstraint string `json:"version_constraint,omitempty"` + ModuleAddress string `json:"module_address,omitempty"` + Expressions map[string]any `json:"expressions,omitempty"` } type ConfigModule struct { @@ -120,13 +120,13 @@ func (m ConfigModule) DigResource(address string) *ConfigResource { } type moduleCall struct { - Source string `json:"source,omitempty"` - Expressions map[string]interface{} `json:"expressions,omitempty"` - CountExpression *expression `json:"count_expression,omitempty"` - ForEachExpression *expression `json:"for_each_expression,omitempty"` - Module ConfigModule `json:"module,omitempty"` - VersionConstraint string `json:"version_constraint,omitempty"` - DependsOn []string `json:"depends_on,omitempty"` + Source string `json:"source,omitempty"` + Expressions map[string]any `json:"expressions,omitempty"` + CountExpression *expression `json:"count_expression,omitempty"` + ForEachExpression *expression `json:"for_each_expression,omitempty"` + Module ConfigModule `json:"module"` + VersionConstraint string `json:"version_constraint,omitempty"` + DependsOn []string `json:"depends_on,omitempty"` } // variables is the JSON representation of the variables provided to the current @@ -164,7 +164,7 @@ type ConfigResource struct { // Expressions" describes the resource-type-specific content of the // configuration block. - Expressions map[string]interface{} `json:"expressions,omitempty"` + Expressions map[string]any `json:"expressions,omitempty"` // SchemaVersion indicates which version of the resource type schema the // "values" property conforms to. @@ -181,14 +181,14 @@ type ConfigResource struct { type output struct { Sensitive bool `json:"sensitive,omitempty"` - Expression expression `json:"expression,omitempty"` + Expression expression `json:"expression"` DependsOn []string `json:"depends_on,omitempty"` Description string `json:"description,omitempty"` } type provisioner struct { - Type string `json:"type,omitempty"` - Expressions map[string]interface{} `json:"expressions,omitempty"` + Type string `json:"type,omitempty"` + Expressions map[string]any `json:"expressions,omitempty"` } // expression represents any unparsed expression @@ -220,7 +220,7 @@ type Variable struct { // prior state (which is always complete) and the planned new state. type StateValues struct { Outputs map[string]Output `json:"outputs,omitempty"` - RootModule Module `json:"root_module,omitempty"` + RootModule Module `json:"root_module"` } // Get a specific resource from this module or its children @@ -293,13 +293,13 @@ type Resource struct { // AttributeValues is the JSON representation of the attribute values of the // resource, whose structure depends on the resource type schema. -type AttributeValues map[string]interface{} +type AttributeValues map[string]any var indexBrackets = regexp.MustCompile(`\[(\d+)\]`) // Digs through the attribute values to find the value at the given key. This // supports nested keys i.e. "foo.bar" and arrays i.e. "foo[0]" -func (av AttributeValues) Dig(key string) (interface{}, bool) { +func (av AttributeValues) Dig(key string) (any, bool) { sections := strings.Split(key, ".") if len(sections) == 0 { @@ -312,7 +312,7 @@ func (av AttributeValues) Dig(key string) (interface{}, bool) { // Check for an index indexMatches := indexBrackets.FindStringSubmatch(section) - var value interface{} + var value any var ok bool if len(indexMatches) == 0 { @@ -339,7 +339,7 @@ func (av AttributeValues) Dig(key string) (interface{}, bool) { } // Check if the value is an array - array, ok := arr.([]interface{}) + array, ok := arr.([]any) if !ok { return nil, false @@ -359,7 +359,7 @@ func (av AttributeValues) Dig(key string) (interface{}, bool) { } // If there are more sections, then we need to dig deeper - childMap, ok := value.(map[string]interface{}) + childMap, ok := value.(map[string]any) if !ok { return nil, false @@ -413,7 +413,7 @@ type ResourceChange struct { Deposed string `json:"deposed,omitempty"` // Change describes the change that will be made to this object - Change Change `json:"change,omitempty"` + Change Change `json:"change"` // ActionReason is a keyword representing some optional extra context // for why the actions in Change.Actions were chosen. diff --git a/tfutils/plan_mapper.go b/tfutils/plan_mapper.go index d79409eb..4b904286 100644 --- a/tfutils/plan_mapper.go +++ b/tfutils/plan_mapper.go @@ -12,10 +12,10 @@ import ( "github.com/getsentry/sentry-go" "github.com/google/uuid" awsAdapters "github.com/overmindtech/cli/aws-source/adapters" - k8sAdapters "github.com/overmindtech/cli/k8s-source/adapters" "github.com/overmindtech/cli/go/sdp-go" - gcpAdapters "github.com/overmindtech/cli/sources/gcp/proc" + k8sAdapters "github.com/overmindtech/cli/k8s-source/adapters" azureAdapters "github.com/overmindtech/cli/sources/azure/proc" + gcpAdapters "github.com/overmindtech/cli/sources/gcp/proc" log "github.com/sirupsen/logrus" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -385,7 +385,7 @@ func mapResourceToQuery(itemDiff *sdp.ItemDiff, terraformResource *Resource, map // isJSONPlanFile checks if the supplied bytes are valid JSON that could be a plan file. // This is used to determine if we need to convert a binary plan or if it's already JSON. func isJSONPlanFile(bytes []byte) bool { - var jsonValue interface{} + var jsonValue any err := json.Unmarshal(bytes, &jsonValue) if err != nil { @@ -400,7 +400,7 @@ func isJSONPlanFile(bytes []byte) bool { // pass a state file to Overmind rather than a plan file since the commands to // create them are similar func isStateFile(bytes []byte) bool { - fields := make(map[string]interface{}) + fields := make(map[string]any) err := json.Unmarshal(bytes, &fields) if err != nil { @@ -651,7 +651,7 @@ func maskSensitiveData(attributes, sensitive any) any { // Finds fields from the `before` and `after` attributes that are known after // apply and replaces the "after" value with the string "(known after apply)" func handleKnownAfterApply(before, after *sdp.ItemAttributes, afterUnknown json.RawMessage) error { - var afterUnknownInterface interface{} + var afterUnknownInterface any err := json.Unmarshal(afterUnknown, &afterUnknownInterface) if err != nil { return fmt.Errorf("could not unmarshal `after_unknown` from plan: %w", err) @@ -684,9 +684,9 @@ func handleKnownAfterApply(before, after *sdp.ItemAttributes, afterUnknown json. // "after" values for fields that are known after apply. By default these are // `null` which produces a bad diff, so we replace them with (known after apply) // to more accurately mirror what Terraform does in the CLI -func insertKnownAfterApply(before, after *structpb.Value, afterUnknown interface{}) error { +func insertKnownAfterApply(before, after *structpb.Value, afterUnknown any) error { switch afterUnknown := afterUnknown.(type) { - case map[string]interface{}: + case map[string]any: for k, v := range afterUnknown { if v == true { if afterFields := after.GetStructValue().GetFields(); afterFields != nil { @@ -711,7 +711,7 @@ func insertKnownAfterApply(before, after *structpb.Value, afterUnknown interface } } } - case []interface{}: + case []any: for i, v := range afterUnknown { if v == true { // If this value in a slice is true, set the corresponding value diff --git a/tfutils/plan_mapper_test.go b/tfutils/plan_mapper_test.go index e6788c2f..8149d8f0 100644 --- a/tfutils/plan_mapper_test.go +++ b/tfutils/plan_mapper_test.go @@ -27,52 +27,52 @@ func TestMapResourceToQuery_PendingCreation(t *testing.T) { t.Parallel() tests := []struct { - name string - itemDiffStatus sdp.ItemDiffStatus - hasMappings bool - expectedMapStatus MapStatus + name string + itemDiffStatus sdp.ItemDiffStatus + hasMappings bool + expectedMapStatus MapStatus expectedMappingStatus sdp.MappedItemMappingStatus - expectMappingError bool + expectMappingError bool }{ { - name: "CREATED with missing attributes - pending creation", - itemDiffStatus: sdp.ItemDiffStatus_ITEM_DIFF_STATUS_CREATED, - hasMappings: true, - expectedMapStatus: MapStatusPendingCreation, + name: "CREATED with missing attributes - pending creation", + itemDiffStatus: sdp.ItemDiffStatus_ITEM_DIFF_STATUS_CREATED, + hasMappings: true, + expectedMapStatus: MapStatusPendingCreation, expectedMappingStatus: sdp.MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_PENDING_CREATION, - expectMappingError: false, + expectMappingError: false, }, { - name: "UPDATED with missing attributes - error", - itemDiffStatus: sdp.ItemDiffStatus_ITEM_DIFF_STATUS_UPDATED, - hasMappings: true, - expectedMapStatus: MapStatusNotEnoughInfo, + name: "UPDATED with missing attributes - error", + itemDiffStatus: sdp.ItemDiffStatus_ITEM_DIFF_STATUS_UPDATED, + hasMappings: true, + expectedMapStatus: MapStatusNotEnoughInfo, expectedMappingStatus: sdp.MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_ERROR, - expectMappingError: true, + expectMappingError: true, }, { - name: "DELETED with missing attributes - error", - itemDiffStatus: sdp.ItemDiffStatus_ITEM_DIFF_STATUS_DELETED, - hasMappings: true, - expectedMapStatus: MapStatusNotEnoughInfo, + name: "DELETED with missing attributes - error", + itemDiffStatus: sdp.ItemDiffStatus_ITEM_DIFF_STATUS_DELETED, + hasMappings: true, + expectedMapStatus: MapStatusNotEnoughInfo, expectedMappingStatus: sdp.MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_ERROR, - expectMappingError: true, + expectMappingError: true, }, { - name: "REPLACED with missing attributes - error", - itemDiffStatus: sdp.ItemDiffStatus_ITEM_DIFF_STATUS_REPLACED, - hasMappings: true, - expectedMapStatus: MapStatusNotEnoughInfo, + name: "REPLACED with missing attributes - error", + itemDiffStatus: sdp.ItemDiffStatus_ITEM_DIFF_STATUS_REPLACED, + hasMappings: true, + expectedMapStatus: MapStatusNotEnoughInfo, expectedMappingStatus: sdp.MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_ERROR, - expectMappingError: true, + expectMappingError: true, }, { - name: "No mappings - unsupported", - itemDiffStatus: sdp.ItemDiffStatus_ITEM_DIFF_STATUS_CREATED, - hasMappings: false, - expectedMapStatus: MapStatusUnsupported, + name: "No mappings - unsupported", + itemDiffStatus: sdp.ItemDiffStatus_ITEM_DIFF_STATUS_CREATED, + hasMappings: false, + expectedMapStatus: MapStatusUnsupported, expectedMappingStatus: sdp.MappedItemMappingStatus_MAPPED_ITEM_MAPPING_STATUS_UNSUPPORTED, - expectMappingError: false, + expectMappingError: false, }, } @@ -629,28 +629,28 @@ func TestMaskSensitiveData(t *testing.T) { } func TestHandleKnownAfterApply(t *testing.T) { - before, err := sdp.ToAttributes(map[string]interface{}{ + before, err := sdp.ToAttributes(map[string]any{ "string_value": "foo", "int_value": 42, "bool_value": true, "float_value": 3.14, "data": "secret", // Known after apply but doesn't exist in the "after" map, this happens sometimes - "list_value": []interface{}{ + "list_value": []any{ "foo", "bar", }, - "map_value": map[string]interface{}{ + "map_value": map[string]any{ "foo": "bar", "bar": "baz", }, - "map_value2": map[string]interface{}{ - "ding": map[string]interface{}{ + "map_value2": map[string]any{ + "ding": map[string]any{ "foo": "bar", }, }, - "nested_list": []interface{}{ - []interface{}{}, - []interface{}{ + "nested_list": []any{ + []any{}, + []any{ "foo", "bar", }, @@ -660,26 +660,26 @@ func TestHandleKnownAfterApply(t *testing.T) { t.Fatal(err) } - after, err := sdp.ToAttributes(map[string]interface{}{ + after, err := sdp.ToAttributes(map[string]any{ "string_value": "bar", // I want to see a diff here "int_value": nil, // These are going to be known after apply "bool_value": nil, // These are going to be known after apply "float_value": 3.14, - "list_value": []interface{}{ + "list_value": []any{ "foo", "bar", "baz", // So is this one }, - "map_value": map[string]interface{}{ // This whole thing will be known after apply + "map_value": map[string]any{ // This whole thing will be known after apply "foo": "bar", }, - "map_value2": map[string]interface{}{ - "ding": map[string]interface{}{ + "map_value2": map[string]any{ + "ding": map[string]any{ "foo": nil, // This will be known after apply }, }, - "nested_list": []interface{}{ - []interface{}{ + "nested_list": []any{ + []any{ "foo", }, }, @@ -748,7 +748,7 @@ func TestHandleKnownAfterApply(t *testing.T) { t.Error(err) } - if list, ok := i.([]interface{}); ok { + if list, ok := i.([]any); ok { if list[2] != KnownAfterApply { t.Errorf("expected third string_value to be %v, got %v", KnownAfterApply, list[2]) } @@ -810,7 +810,7 @@ func interpolateScope(scope string, data map[string]any) (string, error) { } // Digs through a map using the same logic that terraform does i.e. foo.bar[0] -func terraformDig(srcMapPtr interface{}, path string) interface{} { +func terraformDig(srcMapPtr any, path string) any { // Split the path on each period parts := strings.Split(path, ".") @@ -821,7 +821,7 @@ func terraformDig(srcMapPtr interface{}, path string) interface{} { // Check for an index in this section indexMatches := indexBrackets.FindStringSubmatch(parts[0]) - var value interface{} + var value any if len(indexMatches) == 0 { // No index, just get the value @@ -838,7 +838,7 @@ func terraformDig(srcMapPtr interface{}, path string) interface{} { } // Get the value - arr, ok := dig.Interface(srcMapPtr, keyName).([]interface{}) + arr, ok := dig.Interface(srcMapPtr, keyName).([]any) if !ok { return nil @@ -856,13 +856,13 @@ func terraformDig(srcMapPtr interface{}, path string) interface{} { return value } else { // Force it to another map[string]interface{} - valueMap := make(map[string]interface{}) + valueMap := make(map[string]any) if mapString, ok := value.(map[string]string); ok { for k, v := range mapString { valueMap[k] = v } - } else if mapInterface, ok := value.(map[string]interface{}); ok { + } else if mapInterface, ok := value.(map[string]any); ok { valueMap = mapInterface } else if mapAttributeValues, ok := value.(AttributeValues); ok { valueMap = mapAttributeValues From 049ef074f70512d40716fb735b8065768bc38ae4 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Thu, 26 Feb 2026 11:37:32 +0000 Subject: [PATCH 14/74] Add Azure Storage Encryption Scope Client and Adapter (#4014) image --- > [!NOTE] > **Medium Risk** > Adds a new Azure adapter and wires it into global adapter initialization, increasing discovery surface area and Azure API call volume/permissions for storage resources. Logic is read-only and covered by unit tests, but could affect pagination/error handling and linked-resource graphing. > > **Overview** > Adds first-class discovery support for **Azure Storage Encryption Scopes** via a new `EncryptionScopesClient` wrapper and `NewStorageEncryptionScope` adapter (GET by `storageAccount+scopeName`, SEARCH by storage account). > > Wires the new adapter into `manual/adapters.go` (real and placeholder init), links encryption scopes from `storage-account.go` (and updates tests), and extends resource-ID path parsing (`GetResourceIDPathKeys`) to understand `azure-storage-encryption-scope`. Includes generated GoMock client and comprehensive adapter tests (paging, nil-name filtering, and error paths). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit eb69fb4324235c4a3a0de031e325289fe3df6774. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: f9b65d20d0c70fc0cb1f56a402dc708b351f2cff --- .../azure/clients/encryption-scopes-client.go | 35 +++ sources/azure/manual/adapters.go | 10 + sources/azure/manual/storage-account.go | 10 + sources/azure/manual/storage-account_test.go | 6 + .../azure/manual/storage-encryption-scope.go | 259 +++++++++++++++ .../manual/storage-encryption-scope_test.go | 295 ++++++++++++++++++ .../mocks/mock_encryption_scopes_client.go | 72 +++++ sources/azure/shared/utils.go | 1 + 8 files changed, 688 insertions(+) create mode 100644 sources/azure/clients/encryption-scopes-client.go create mode 100644 sources/azure/manual/storage-encryption-scope.go create mode 100644 sources/azure/manual/storage-encryption-scope_test.go create mode 100644 sources/azure/shared/mocks/mock_encryption_scopes_client.go diff --git a/sources/azure/clients/encryption-scopes-client.go b/sources/azure/clients/encryption-scopes-client.go new file mode 100644 index 00000000..bd730639 --- /dev/null +++ b/sources/azure/clients/encryption-scopes-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" +) + +//go:generate mockgen -destination=../shared/mocks/mock_encryption_scopes_client.go -package=mocks -source=encryption-scopes-client.go + +// EncryptionScopesPager is a type alias for the generic Pager interface with encryption scope list response type. +type EncryptionScopesPager = Pager[armstorage.EncryptionScopesClientListResponse] + +// EncryptionScopesClient is an interface for interacting with Azure storage encryption scopes +type EncryptionScopesClient interface { + Get(ctx context.Context, resourceGroupName string, accountName string, encryptionScopeName string) (armstorage.EncryptionScopesClientGetResponse, error) + List(ctx context.Context, resourceGroupName string, accountName string) EncryptionScopesPager +} + +type encryptionScopesClient struct { + client *armstorage.EncryptionScopesClient +} + +func (c *encryptionScopesClient) Get(ctx context.Context, resourceGroupName string, accountName string, encryptionScopeName string) (armstorage.EncryptionScopesClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, accountName, encryptionScopeName, nil) +} + +func (c *encryptionScopesClient) List(ctx context.Context, resourceGroupName string, accountName string) EncryptionScopesPager { + return c.client.NewListPager(resourceGroupName, accountName, nil) +} + +// NewEncryptionScopesClient creates a new EncryptionScopesClient from the Azure SDK client +func NewEncryptionScopesClient(client *armstorage.EncryptionScopesClient) EncryptionScopesClient { + return &encryptionScopesClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 6fc5f87c..5a9c03b3 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -106,6 +106,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create tables client: %w", err) } + encryptionScopesClient, err := armstorage.NewEncryptionScopesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create encryption scopes client: %w", err) + } + virtualNetworksClient, err := armnetwork.NewVirtualNetworksClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create virtual networks client: %w", err) @@ -325,6 +330,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewTablesClient(tablesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewStorageEncryptionScope( + clients.NewEncryptionScopesClient(encryptionScopesClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewNetworkVirtualNetwork( clients.NewVirtualNetworksClient(virtualNetworksClient), resourceGroupScopes, @@ -506,6 +515,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewStorageFileShare(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewStorageQueues(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewStorageTable(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewStorageEncryptionScope(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkVirtualNetwork(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkSubnet(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkNetworkInterface(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/storage-account.go b/sources/azure/manual/storage-account.go index 5ab37a6e..b5e89808 100644 --- a/sources/azure/manual/storage-account.go +++ b/sources/azure/manual/storage-account.go @@ -172,6 +172,15 @@ func (s storageAccountWrapper) azureStorageAccountToSDPItem(account *armstorage. }, }) + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageEncryptionScope.String(), + Method: sdp.QueryMethod_SEARCH, + Query: accountName, + Scope: scope, + }, + }) + // Link to Private Endpoint Connections (child resource) // Reference: https://learn.microsoft.com/en-us/rest/api/storagerp/private-endpoint-connections/list?view=rest-storagerp-2025-06-01 // Private endpoint connections can be listed using the storage account name @@ -443,6 +452,7 @@ func (s storageAccountWrapper) PotentialLinks() map[shared.ItemType]bool { azureshared.StorageFileShare: true, azureshared.StorageTable: true, azureshared.StorageQueue: true, + azureshared.StorageEncryptionScope: true, azureshared.StoragePrivateEndpointConnection: true, // External resources azureshared.ManagedIdentityUserAssignedIdentity: true, diff --git a/sources/azure/manual/storage-account_test.go b/sources/azure/manual/storage-account_test.go index 3dac2006..860b0b75 100644 --- a/sources/azure/manual/storage-account_test.go +++ b/sources/azure/manual/storage-account_test.go @@ -86,6 +86,12 @@ func TestStorageAccount(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: accountName, ExpectedScope: subscriptionID + "." + resourceGroup, + }, { + // Storage encryption scope link (child resource) + ExpectedType: azureshared.StorageEncryptionScope.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: accountName, + ExpectedScope: subscriptionID + "." + resourceGroup, }, { // Storage private endpoint connection link (child resource) ExpectedType: azureshared.StoragePrivateEndpointConnection.String(), diff --git a/sources/azure/manual/storage-encryption-scope.go b/sources/azure/manual/storage-encryption-scope.go new file mode 100644 index 00000000..ea8dcb0e --- /dev/null +++ b/sources/azure/manual/storage-encryption-scope.go @@ -0,0 +1,259 @@ +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" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var StorageEncryptionScopeLookupByName = shared.NewItemTypeLookup("name", azureshared.StorageEncryptionScope) + +type storageEncryptionScopeWrapper struct { + client clients.EncryptionScopesClient + + *azureshared.MultiResourceGroupBase +} + +func NewStorageEncryptionScope(client clients.EncryptionScopesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &storageEncryptionScopeWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_STORAGE, + azureshared.StorageEncryptionScope, + ), + } +} + +func (s storageEncryptionScopeWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: storageAccountName and encryptionScopeName", + Scope: scope, + ItemType: s.Type(), + } + } + storageAccountName := queryParts[0] + encryptionScopeName := queryParts[1] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + resp, err := s.client.Get(ctx, rgScope.ResourceGroup, storageAccountName, encryptionScopeName) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + item, sdpErr := s.azureEncryptionScopeToSDPItem(&resp.EncryptionScope, storageAccountName, encryptionScopeName, scope) + if sdpErr != nil { + return nil, sdpErr + } + + return item, nil +} + +func (s storageEncryptionScopeWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + StorageAccountLookupByName, + StorageEncryptionScopeLookupByName, + } +} + +func (s storageEncryptionScopeWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: storageAccountName", + Scope: scope, + ItemType: s.Type(), + } + } + storageAccountName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + pager := s.client.List(ctx, rgScope.ResourceGroup, storageAccountName) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + for _, encScope := range page.Value { + if encScope.Name == nil { + continue + } + + item, sdpErr := s.azureEncryptionScopeToSDPItem(encScope, storageAccountName, *encScope.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (s storageEncryptionScopeWrapper) 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 _, encScope := range page.Value { + if encScope.Name == nil { + continue + } + item, sdpErr := s.azureEncryptionScopeToSDPItem(encScope, storageAccountName, *encScope.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (s storageEncryptionScopeWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + StorageAccountLookupByName, + }, + } +} + +func (s storageEncryptionScopeWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.StorageAccount: true, + azureshared.KeyVaultVault: true, + azureshared.KeyVaultKey: true, + stdlib.NetworkDNS: true, + } +} + +func (s storageEncryptionScopeWrapper) azureEncryptionScopeToSDPItem(encScope *armstorage.EncryptionScope, storageAccountName, encryptionScopeName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(encScope, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(storageAccountName, encryptionScopeName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + item := &sdp.Item{ + Type: azureshared.StorageEncryptionScope.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + } + + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageAccount.String(), + Method: sdp.QueryMethod_GET, + Query: storageAccountName, + Scope: scope, + }, + }) + + // Link to Key Vault when encryption scope uses customer-managed keys (source Microsoft.KeyVault) + if encScope.EncryptionScopeProperties != nil && encScope.EncryptionScopeProperties.KeyVaultProperties != nil && encScope.EncryptionScopeProperties.KeyVaultProperties.KeyURI != nil { + keyURI := *encScope.EncryptionScopeProperties.KeyVaultProperties.KeyURI + vaultName := azureshared.ExtractVaultNameFromURI(keyURI) + keyName := azureshared.ExtractKeyNameFromURI(keyURI) + if vaultName != "" { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultVault.String(), + Method: sdp.QueryMethod_GET, + Query: vaultName, + Scope: scope, + }, + }) + } + if vaultName != "" && keyName != "" { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultKey.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(vaultName, keyName), + Scope: scope, + }, + }) + } + if dnsName := azureshared.ExtractDNSFromURL(keyURI); dnsName != "" { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: dnsName, + Scope: "global", + }, + }) + } + } + + if encScope.EncryptionScopeProperties != nil && encScope.EncryptionScopeProperties.State != nil { + switch *encScope.EncryptionScopeProperties.State { + case armstorage.EncryptionScopeStateEnabled: + item.Health = sdp.Health_HEALTH_OK.Enum() + case armstorage.EncryptionScopeStateDisabled: + item.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + default: + item.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + return item, nil +} + +func (s storageEncryptionScopeWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_storage_encryption_scope.id", + }, + } +} + +func (s storageEncryptionScopeWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Storage/storageAccounts/encryptionScopes/read", + } +} + +func (s storageEncryptionScopeWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/storage-encryption-scope_test.go b/sources/azure/manual/storage-encryption-scope_test.go new file mode 100644 index 00000000..4af992a1 --- /dev/null +++ b/sources/azure/manual/storage-encryption-scope_test.go @@ -0,0 +1,295 @@ +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/storage/armstorage/v3" + "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" +) + +type mockEncryptionScopesPager struct { + pages []armstorage.EncryptionScopesClientListResponse + index int +} + +func (m *mockEncryptionScopesPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockEncryptionScopesPager) NextPage(ctx context.Context) (armstorage.EncryptionScopesClientListResponse, error) { + if m.index >= len(m.pages) { + return armstorage.EncryptionScopesClientListResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type errorEncryptionScopesPager struct{} + +func (e *errorEncryptionScopesPager) More() bool { + return true +} + +func (e *errorEncryptionScopesPager) NextPage(ctx context.Context) (armstorage.EncryptionScopesClientListResponse, error) { + return armstorage.EncryptionScopesClientListResponse{}, errors.New("pager error") +} + +type testEncryptionScopesClient struct { + *mocks.MockEncryptionScopesClient + pager clients.EncryptionScopesPager +} + +func (t *testEncryptionScopesClient) List(ctx context.Context, resourceGroupName, accountName string) clients.EncryptionScopesPager { + return t.pager +} + +func TestStorageEncryptionScope(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + storageAccountName := "teststorageaccount" + encryptionScopeName := "test-encryption-scope" + + t.Run("Get", func(t *testing.T) { + encScope := createAzureEncryptionScope(encryptionScopeName) + + mockClient := mocks.NewMockEncryptionScopesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, storageAccountName, encryptionScopeName).Return( + armstorage.EncryptionScopesClientGetResponse{ + EncryptionScope: *encScope, + }, nil) + + testClient := &testEncryptionScopesClient{MockEncryptionScopesClient: mockClient} + wrapper := manual.NewStorageEncryptionScope(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(storageAccountName, encryptionScopeName) + 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.StorageEncryptionScope.String() { + t.Errorf("Expected type %s, got %s", azureshared.StorageEncryptionScope.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != shared.CompositeLookupKey(storageAccountName, encryptionScopeName) { + t.Errorf("Expected unique attribute value %s, got %s", shared.CompositeLookupKey(storageAccountName, encryptionScopeName), sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + linkedQueries := sdpItem.GetLinkedItemQueries() + if len(linkedQueries) != 1 { + t.Fatalf("Expected 1 linked query, got: %d", len(linkedQueries)) + } + + linkedQuery := linkedQueries[0] + if linkedQuery.GetQuery().GetType() != azureshared.StorageAccount.String() { + t.Errorf("Expected linked query type %s, got %s", azureshared.StorageAccount.String(), linkedQuery.GetQuery().GetType()) + } + if linkedQuery.GetQuery().GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected linked query method GET, got %s", linkedQuery.GetQuery().GetMethod()) + } + if linkedQuery.GetQuery().GetQuery() != storageAccountName { + t.Errorf("Expected linked query %s, got %s", storageAccountName, linkedQuery.GetQuery().GetQuery()) + } + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockEncryptionScopesClient(ctrl) + testClient := &testEncryptionScopesClient{MockEncryptionScopesClient: mockClient} + + wrapper := manual.NewStorageEncryptionScope(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], storageAccountName, true) + if qErr == nil { + t.Error("Expected error when providing insufficient query parts, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + scope1 := createAzureEncryptionScope("scope-1") + scope2 := createAzureEncryptionScope("scope-2") + + mockClient := mocks.NewMockEncryptionScopesClient(ctrl) + mockPager := &mockEncryptionScopesPager{ + pages: []armstorage.EncryptionScopesClientListResponse{ + { + EncryptionScopeListResult: armstorage.EncryptionScopeListResult{ + Value: []*armstorage.EncryptionScope{scope1, scope2}, + }, + }, + }, + } + + testClient := &testEncryptionScopesClient{ + MockEncryptionScopesClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewStorageEncryptionScope(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, wrapper.Scopes()[0], storageAccountName, 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.Fatalf("Expected no validation error, got: %v", err) + } + if item.GetType() != azureshared.StorageEncryptionScope.String() { + t.Errorf("Expected type %s, got %s", azureshared.StorageEncryptionScope.String(), item.GetType()) + } + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockEncryptionScopesClient(ctrl) + testClient := &testEncryptionScopesClient{MockEncryptionScopesClient: mockClient} + + wrapper := manual.NewStorageEncryptionScope(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("Search_ScopeWithNilName", func(t *testing.T) { + mockClient := mocks.NewMockEncryptionScopesClient(ctrl) + validScope := createAzureEncryptionScope("valid-scope") + mockPager := &mockEncryptionScopesPager{ + pages: []armstorage.EncryptionScopesClientListResponse{ + { + EncryptionScopeListResult: armstorage.EncryptionScopeListResult{ + Value: []*armstorage.EncryptionScope{ + {Name: nil}, + validScope, + }, + }, + }, + }, + } + + testClient := &testEncryptionScopesClient{ + MockEncryptionScopesClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewStorageEncryptionScope(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, wrapper.Scopes()[0], storageAccountName, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 1 { + t.Fatalf("Expected 1 item, got: %d", len(sdpItems)) + } + + if sdpItems[0].UniqueAttributeValue() != shared.CompositeLookupKey(storageAccountName, "valid-scope") { + t.Errorf("Expected unique value %s, got %s", shared.CompositeLookupKey(storageAccountName, "valid-scope"), sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("encryption scope not found") + + mockClient := mocks.NewMockEncryptionScopesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, storageAccountName, "nonexistent-scope").Return( + armstorage.EncryptionScopesClientGetResponse{}, expectedErr) + + testClient := &testEncryptionScopesClient{MockEncryptionScopesClient: mockClient} + wrapper := manual.NewStorageEncryptionScope(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := storageAccountName + shared.QuerySeparator + "nonexistent-scope" + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent encryption scope, but got nil") + } + }) + + t.Run("ErrorHandling_Search", func(t *testing.T) { + mockClient := mocks.NewMockEncryptionScopesClient(ctrl) + errorPager := &errorEncryptionScopesPager{} + + testClient := &testEncryptionScopesClient{ + MockEncryptionScopesClient: mockClient, + pager: errorPager, + } + + wrapper := manual.NewStorageEncryptionScope(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, wrapper.Scopes()[0], storageAccountName, true) + if err == nil { + t.Error("Expected error from pager when NextPage returns an error, but got nil") + } + }) +} + +func createAzureEncryptionScope(scopeName string) *armstorage.EncryptionScope { + return &armstorage.EncryptionScope{ + ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/encryptionScopes/" + scopeName), + Name: to.Ptr(scopeName), + Type: to.Ptr("Microsoft.Storage/storageAccounts/encryptionScopes"), + EncryptionScopeProperties: &armstorage.EncryptionScopeProperties{ + Source: to.Ptr(armstorage.EncryptionScopeSourceMicrosoftStorage), + State: to.Ptr(armstorage.EncryptionScopeStateEnabled), + }, + } +} diff --git a/sources/azure/shared/mocks/mock_encryption_scopes_client.go b/sources/azure/shared/mocks/mock_encryption_scopes_client.go new file mode 100644 index 00000000..8f8b17b0 --- /dev/null +++ b/sources/azure/shared/mocks/mock_encryption_scopes_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: encryption-scopes-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_encryption_scopes_client.go -package=mocks -source=encryption-scopes-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockEncryptionScopesClient is a mock of EncryptionScopesClient interface. +type MockEncryptionScopesClient struct { + ctrl *gomock.Controller + recorder *MockEncryptionScopesClientMockRecorder + isgomock struct{} +} + +// MockEncryptionScopesClientMockRecorder is the mock recorder for MockEncryptionScopesClient. +type MockEncryptionScopesClientMockRecorder struct { + mock *MockEncryptionScopesClient +} + +// NewMockEncryptionScopesClient creates a new mock instance. +func NewMockEncryptionScopesClient(ctrl *gomock.Controller) *MockEncryptionScopesClient { + mock := &MockEncryptionScopesClient{ctrl: ctrl} + mock.recorder = &MockEncryptionScopesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockEncryptionScopesClient) EXPECT() *MockEncryptionScopesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockEncryptionScopesClient) Get(ctx context.Context, resourceGroupName, accountName, encryptionScopeName string) (armstorage.EncryptionScopesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, accountName, encryptionScopeName) + ret0, _ := ret[0].(armstorage.EncryptionScopesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockEncryptionScopesClientMockRecorder) Get(ctx, resourceGroupName, accountName, encryptionScopeName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockEncryptionScopesClient)(nil).Get), ctx, resourceGroupName, accountName, encryptionScopeName) +} + +// List mocks base method. +func (m *MockEncryptionScopesClient) List(ctx context.Context, resourceGroupName, accountName string) clients.EncryptionScopesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, resourceGroupName, accountName) + ret0, _ := ret[0].(clients.EncryptionScopesPager) + return ret0 +} + +// List indicates an expected call of List. +func (mr *MockEncryptionScopesClientMockRecorder) List(ctx, resourceGroupName, accountName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockEncryptionScopesClient)(nil).List), ctx, resourceGroupName, accountName) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index d1d51362..94438104 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -20,6 +20,7 @@ func GetResourceIDPathKeys(resourceType string) []string { pathKeysMap := map[string][]string{ "azure-storage-queue": {"storageAccounts", "queues"}, "azure-storage-blob-container": {"storageAccounts", "containers"}, + "azure-storage-encryption-scope": {"storageAccounts", "encryptionScopes"}, "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", From 52f8c5c11b8a6ba3e86aa7702b7ae33a0307f0ef Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Thu, 26 Feb 2026 13:13:47 +0000 Subject: [PATCH 15/74] Add Azure documentation for configuration and category (#4015) (#4017) GitOrigin-RevId: ae97ce2892c7961c34eaacb2ba7f01d9968b4bc0 --- .../docs/sources/azure/_category_.json | 1 + .../docs/sources/azure/configuration.md | 59 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 docs.overmind.tech/docs/sources/azure/_category_.json create mode 100644 docs.overmind.tech/docs/sources/azure/configuration.md diff --git a/docs.overmind.tech/docs/sources/azure/_category_.json b/docs.overmind.tech/docs/sources/azure/_category_.json new file mode 100644 index 00000000..694c142d --- /dev/null +++ b/docs.overmind.tech/docs/sources/azure/_category_.json @@ -0,0 +1 @@ +{"label":"Azure","position":4,"collapsed":true,"link":{"type":"generated-index","description":"How to integrate your Azure subscription."}} diff --git a/docs.overmind.tech/docs/sources/azure/configuration.md b/docs.overmind.tech/docs/sources/azure/configuration.md new file mode 100644 index 00000000..d0cf0ca9 --- /dev/null +++ b/docs.overmind.tech/docs/sources/azure/configuration.md @@ -0,0 +1,59 @@ +--- +title: Azure Configuration +sidebar_position: 1 +--- + +# Azure Configuration + +## Overview + +Overmind's Azure infrastructure discovery provides visibility into your Microsoft Azure resources through secure, read-only access. Overmind uses an Azure AD App Registration with federated credentials (workload identity) when running the source for you—no client secrets are stored or entered in the UI. + +To connect an Azure source, you need a **Name** (friendly label in Overmind), **Subscription ID**, **Tenant ID**, and **Client ID**. Overmind only ever requests read-only access (minimum **Reader** role on the subscription). + +## Prerequisites + +- **Azure subscription**: An active subscription you want to discover. +- **Azure AD App Registration**: An app registered in Azure AD with at least **Reader** role on the subscription (used for workload identity; no client secret is required in the Overmind UI). +- **Permissions**: Ability to create an App Registration and assign roles in the subscription (e.g. Owner or User Access Administrator). + +## Where to get the IDs + +You need three values from Azure. All are GUIDs. + +### Subscription ID + +- **Azure Portal:** In the portal, go to **Cost Management + Billing** → **Subscriptions** (or see [View subscriptions in the Azure portal](https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/view-all-accounts)), select your subscription, and copy **Subscription ID**. +- **Azure CLI:** Run `az account show --query id -o tsv` (after `az login` and, if needed, `az account set --subscription "your-subscription-name-or-id"`). + +### Tenant ID + +- **Azure Portal:** See [Find your Azure AD tenant ID](https://learn.microsoft.com/en-us/azure/active-directory/fundamentals/active-directory-how-to-find-tenant) — in the portal, go to **Azure Active Directory** → **Overview** and copy **Tenant ID**. +- **Azure CLI:** Run `az account show --query tenantId -o tsv`. + +### Client ID (Application ID) + +- **Azure Portal:** See [Register an application](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) — in **Azure Active Directory** → **App registrations**, select your app (or create one) and copy **Application (client) ID**. +- **If you create a service principal via CLI:** The **appId** in the command output is your Client ID. + +Your app must have at least **Reader** on the subscription. For Overmind’s managed source we use federated credentials (workload identity), so you do **not** need to create or paste a client secret in Overmind. + +For detailed setup (e.g. App Registration, role assignment, federated credentials), see [Microsoft’s documentation on registering an application](https://learn.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app) and [Reader role](https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#reader). + +## Add an Azure source in Overmind + +1. In Overmind, go to **Settings** (profile menu) → **Sources** → **Add source** → **Azure**. +2. Enter a **Name** (e.g. "Production Azure") so you can identify the source in Overmind. +3. Enter **Subscription ID**, **Tenant ID**, and **Client ID** using the values from [Where to get the IDs](#where-to-get-the-ids) above. +4. (Optional) **Regions:** Select specific Azure regions to limit discovery. If you leave this empty, Overmind discovers resources in all regions in the subscription. +5. Click **Create source**. + +The source will appear in your Sources list. Once the connection is established, its status will show as healthy and you can use it in Explore and change analysis. + +## Check your sources + +After you have configured a source, it will appear under [Settings → Sources](https://app.overmind.tech/settings/sources). There you can confirm the source is healthy and view its details (Source UUID, Subscription ID, Tenant ID, Client ID, and Regions). + +## Explore your data + +Once your Azure source is healthy, go to the [Explore page](https://app.overmind.tech/explore) to browse your Azure resources and their relationships. From c5c203f433aaa131626e693fe31e72412aecb069 Mon Sep 17 00:00:00 2001 From: TP Honey Date: Thu, 26 Feb 2026 14:22:15 +0000 Subject: [PATCH 16/74] =?UTF-8?q?[ENG-2789]=20Prevent=20sources=20reportin?= =?UTF-8?q?g=20healthy=20before=20adapters=20are=20init=E2=80=A6=20(#4013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image image ## Summary - Closes the race window where sources report HEALTHY to the API server (via heartbeats and readiness probes) before adapters are registered, which caused silent mapping failures (ENG-2786) - Introduces an `adaptersInitialized` flag (`atomic.Bool`) in the discovery `Engine` that gates both `ReadinessHealthCheck` and `SendHeartbeat` - All source entry points (CLI explore, stdlib, snapshot, and `InitialiseAdapters`) now explicitly mark adapters as initialized after successful setup ## Linear Ticket - **Ticket**: [ENG-2789](https://linear.app/overmind/issue/ENG-2789/source-reports-healthy-before-adapters-are-initialized) — Source reports HEALTHY before adapters are initialized - **Purpose**: Prevent premature healthy status that causes mapping queries to silently return 0 items - **Related**: [ENG-2786](https://linear.app/overmind/issue/ENG-2786/investigate-failed-mappings) (root cause investigation), [ENG-2806](https://linear.app/overmind/issue/ENG-2806) (tracking ticket) ## Changes **Core fix** (`go/discovery/`): - `engine.go`: Added `adaptersInitialized atomic.Bool` field, `MarkAdaptersInitialized()` and `AreAdaptersInitialized()` methods. `ReadinessHealthCheck` returns an error when the flag is unset. `InitialiseAdapters` sets the flag on success. - `heartbeat.go`: `SendHeartbeat` includes "adapters not yet initialized" in the error string while the flag is unset, so the API server marks the source as UNHEALTHY during startup. - `doc.go`: Added "Readiness gating" section documenting the new contract. **Source entry points**: - `stdlib-source/cmd/root.go`: Calls `MarkAdaptersInitialized()` after successful init - `sources/snapshot/cmd/root.go`: Same - `cli/cmd/explore.go`: Calls `MarkAdaptersInitialized()` for all five engine types (snapshot, stdlib, AWS, GCP, Azure) **Tests** (`go/discovery/`): - `engine_initerror_test.go`: 6 new tests covering the flag lifecycle; existing tests updated to call `MarkAdaptersInitialized()` so they isolate `initError` behavior - `heartbeat_test.go`: Existing test updated to initialize the flag in setup Sources that use `InitialiseAdapters` (AWS, GCP, Azure, Harness, k8s) get the flag set automatically — no changes needed. ## Test plan - [x] `go test ./go/discovery/ -run 'TestReadiness|TestHeartbeat|TestInitialiseAdapters' -v` — all pass - [x] `go build ./cli/...` — compiles cleanly - [x] `go vet ./go/discovery/...` — clean - [x] ran everything locally + an extra source, explore worked in the UI - [x] CI passes on all affected packages --- > [!NOTE] > **Medium Risk** > Changes source health signaling semantics: pods will now report unready/unhealthy until adapter init completes, which could affect rollout behavior or monitoring if any entrypoint forgets to mark initialization. > > **Overview** > Prevents sources from reporting healthy before adapters are registered by introducing an `adaptersInitialized` flag on the discovery `Engine` and gating both `ReadinessHealthCheck` and `SendHeartbeat` on it. > > `InitialiseAdapters` now marks adapters initialized on success, and source entry points (CLI `explore`, `snapshot-source`, `stdlib-source`) explicitly call `MarkAdaptersInitialized()` after successful adapter setup (with snapshot/stdlib also sending an immediate post-init heartbeat). Tests and docs are updated to cover and document the new readiness/heartbeat contract. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c9af3c42d54ad809c2da9c02d8ab9714256065bd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: af146952982a8552394d0f7c6a8fd7401d0e93de --- cmd/explore.go | 5 + go/discovery/doc.go | 13 +- go/discovery/engine.go | 26 ++++ go/discovery/engine_initerror_test.go | 209 ++++++++++++++++++++++++++ go/discovery/heartbeat.go | 4 + go/discovery/heartbeat_test.go | 1 + sources/snapshot/cmd/root.go | 8 +- stdlib-source/cmd/root.go | 8 +- 8 files changed, 271 insertions(+), 3 deletions(-) diff --git a/cmd/explore.go b/cmd/explore.go index 20df5072..76711032 100644 --- a/cmd/explore.go +++ b/cmd/explore.go @@ -104,6 +104,7 @@ func StartLocalSources(ctx context.Context, oi sdp.OvermindInstance, token *oaut snapshotSpinner.Fail(fmt.Sprintf("Failed to initialize snapshot source adapters: %v", err)) return func() {}, fmt.Errorf("failed to initialize snapshot source adapters: %w", err) } + snapshotEngine.MarkAdaptersInitialized() err = snapshotEngine.Start(ctx) if err != nil { snapshotSpinner.Fail(fmt.Sprintf("Failed to start snapshot source engine: %v", err)) @@ -182,6 +183,7 @@ func StartLocalSources(ctx context.Context, oi sdp.OvermindInstance, token *oaut return nil, fmt.Errorf("failed to initialize stdlib source adapters: %w", err) } // todo: pass in context with timeout to abort timely and allow Ctrl-C to work + stdlibEngine.MarkAdaptersInitialized() err = stdlibEngine.Start(ctx) if err != nil { stdlibSpinner.Fail("Failed to start stdlib source engine") @@ -280,6 +282,7 @@ func StartLocalSources(ctx context.Context, oi sdp.OvermindInstance, token *oaut return nil, fmt.Errorf("failed to initialize AWS source adapters: %w", err) } + awsEngine.MarkAdaptersInitialized() err = awsEngine.Start(ctx) if err != nil { awsSpinner.Fail("Failed to start AWS source engine") @@ -389,6 +392,7 @@ func StartLocalSources(ctx context.Context, oi sdp.OvermindInstance, token *oaut continue // Skip this engine but continue with others } + gcpEngine.MarkAdaptersInitialized() err = gcpEngine.Start(ctx) if err != nil { if gcpConfig == nil { @@ -535,6 +539,7 @@ func StartLocalSources(ctx context.Context, oi sdp.OvermindInstance, token *oaut continue // Skip this engine but continue with others } + azureEngine.MarkAdaptersInitialized() err = azureEngine.Start(ctx) if err != nil { statusArea.Println(fmt.Sprintf("Failed to start Azure source for subscription %s: %s", azureConfig.SubscriptionID, err.Error())) diff --git a/go/discovery/doc.go b/go/discovery/doc.go index ab24ca1e..8d3d355f 100644 --- a/go/discovery/doc.go +++ b/go/discovery/doc.go @@ -14,6 +14,17 @@ // 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() // +// # Readiness gating +// +// The engine defaults to "not ready" until adapters are initialized. Both +// ReadinessHealthCheck (the /healthz/ready HTTP probe) and SendHeartbeat report +// an error while adaptersInitialized is false. This prevents Kubernetes from +// routing traffic to a pod that has no adapters registered. +// +// InitialiseAdapters calls MarkAdaptersInitialized automatically on success. +// Sources that do their own initialization (without InitialiseAdapters) must +// call MarkAdaptersInitialized explicitly after adding adapters. +// // # Error handling // // Fatal errors (caller must return or exit): EngineConfigFromViper, NewEngine, Start. @@ -29,5 +40,5 @@ // 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. +// See SetInitError, MarkAdaptersInitialized, and InitialiseAdapters for details and examples. package discovery diff --git a/go/discovery/engine.go b/go/discovery/engine.go index 2b3df9db..e5a1dfbb 100644 --- a/go/discovery/engine.go +++ b/go/discovery/engine.go @@ -8,6 +8,7 @@ import ( "slices" "strings" "sync" + "sync/atomic" "time" "connectrpc.com/connect" @@ -161,6 +162,13 @@ type Engine struct { // CrashLoopBackOff so customers can diagnose and fix configuration issues. initError error initErrorMutex sync.RWMutex + + // adaptersInitialized tracks whether adapters have been successfully registered. + // Defaults to false; set to true by InitialiseAdapters on success or manually + // via MarkAdaptersInitialized for sources that don't use InitialiseAdapters. + // ReadinessHealthCheck and SendHeartbeat both check this flag so that a source + // cannot report healthy before it can actually serve queries. + adaptersInitialized atomic.Bool } func NewEngine(engineConfig *EngineConfig) (*Engine, error) { @@ -501,6 +509,10 @@ func (e *Engine) ReadinessHealthCheck(ctx context.Context) error { attribute.String("ovm.healthcheck.type", "readiness"), ) + if !e.AreAdaptersInitialized() { + return errors.New("adapters not yet initialized") + } + // Check for persistent initialization errors first if initErr := e.GetInitError(); initErr != nil { return fmt.Errorf("source initialization failed: %w", initErr) @@ -764,6 +776,19 @@ func (e *Engine) GetInitError() error { return e.initError } +// MarkAdaptersInitialized records that adapters have been successfully registered +// and the source is ready to serve queries. This is called automatically by +// InitialiseAdapters on success. Sources that do their own initialization +// (without InitialiseAdapters) must call this explicitly after adding adapters. +func (e *Engine) MarkAdaptersInitialized() { + e.adaptersInitialized.Store(true) +} + +// AreAdaptersInitialized reports whether adapters have been successfully registered. +func (e *Engine) AreAdaptersInitialized() bool { + return e.adaptersInitialized.Load() +} + // InitialiseAdapters retries initFn with exponential backoff (capped at // 5 minutes) until it succeeds or ctx is cancelled. It blocks the caller. // @@ -807,6 +832,7 @@ func (e *Engine) InitialiseAdapters(ctx context.Context, initFn func(ctx context // Clear any previous init error before the heartbeat so the // API/UI immediately sees the healthy status. e.SetInitError(nil) + e.MarkAdaptersInitialized() } // Send heartbeat regardless of outcome so the API/UI reflects current status diff --git a/go/discovery/engine_initerror_test.go b/go/discovery/engine_initerror_test.go index 97e1ff07..71d35309 100644 --- a/go/discovery/engine_initerror_test.go +++ b/go/discovery/engine_initerror_test.go @@ -124,6 +124,9 @@ func TestReadinessHealthCheckWithInitError(t *testing.T) { t.Fatalf("failed to create engine: %v", err) } + // Mark adapters initialized so we're only testing initError behavior + e.MarkAdaptersInitialized() + ctx := context.Background() // Readiness should pass when no init error @@ -176,6 +179,9 @@ func TestSendHeartbeatWithInitError(t *testing.T) { t.Fatalf("failed to create engine: %v", err) } + // Mark adapters initialized so we're only testing initError behavior + e.MarkAdaptersInitialized() + ctx := context.Background() // Send heartbeat with init error @@ -221,6 +227,9 @@ func TestSendHeartbeatWithInitErrorAndCustomError(t *testing.T) { t.Fatalf("failed to create engine: %v", err) } + // Mark adapters initialized so we're only testing initError + custom error behavior + e.MarkAdaptersInitialized() + ctx := context.Background() // Set init error and send heartbeat with custom error @@ -284,6 +293,9 @@ func TestInitialiseAdapters_Success(t *testing.T) { if err := e.GetInitError(); err != nil { t.Errorf("expected init error to be cleared after success, got: %v", err) } + if !e.AreAdaptersInitialized() { + t.Error("expected adaptersInitialized to be true after successful InitialiseAdapters") + } } func TestInitialiseAdapters_RetryThenSuccess(t *testing.T) { @@ -361,3 +373,200 @@ func TestInitialiseAdapters_ContextCancelled(t *testing.T) { t.Error("expected init error to be set after context cancellation with failures") } } + +func TestReadinessFailsBeforeInitialization(t *testing.T) { + ec := &EngineConfig{ + EngineType: "test", + SourceName: "test-source", + HeartbeatOptions: &HeartbeatOptions{ + ReadinessCheck: func(ctx context.Context) error { + return nil + }, + }, + } + + e, err := NewEngine(ec) + if err != nil { + t.Fatalf("failed to create engine: %v", err) + } + + ctx := context.Background() + + err = e.ReadinessHealthCheck(ctx) + if err == nil { + t.Fatal("expected readiness to fail before adapters initialized, got nil") + } + if !strings.Contains(err.Error(), "adapters not yet initialized") { + t.Errorf("expected error to contain 'adapters not yet initialized', got: %v", err) + } +} + +func TestReadinessPassesAfterInitialization(t *testing.T) { + ec := &EngineConfig{ + EngineType: "test", + SourceName: "test-source", + HeartbeatOptions: &HeartbeatOptions{ + ReadinessCheck: func(ctx context.Context) error { + return nil + }, + }, + } + + e, err := NewEngine(ec) + if err != nil { + t.Fatalf("failed to create engine: %v", err) + } + + e.MarkAdaptersInitialized() + + ctx := context.Background() + + if err := e.ReadinessHealthCheck(ctx); err != nil { + t.Errorf("expected readiness to pass after MarkAdaptersInitialized, got: %v", err) + } +} + +func TestHeartbeatIncludesUninitializedError(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) + } + + // Do NOT call MarkAdaptersInitialized -- engine is freshly created + + responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ + Msg: &sdp.SubmitSourceHeartbeatResponse{}, + } + + ctx := context.Background() + err = e.SendHeartbeat(ctx, nil) + if err != nil { + t.Fatalf("expected SendHeartbeat to succeed, got: %v", err) + } + + req := <-requests + if req.Msg.GetError() == "" { + t.Fatal("expected heartbeat to include error before initialization, got empty string") + } + if !strings.Contains(req.Msg.GetError(), "adapters not yet initialized") { + t.Errorf("expected heartbeat error to contain 'adapters not yet initialized', got: %q", req.Msg.GetError()) + } +} + +func TestHeartbeatClearsAfterInitialization(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) + } + + e.MarkAdaptersInitialized() + + responses <- &connect.Response[sdp.SubmitSourceHeartbeatResponse]{ + Msg: &sdp.SubmitSourceHeartbeatResponse{}, + } + + ctx := context.Background() + err = e.SendHeartbeat(ctx, nil) + if err != nil { + t.Fatalf("expected SendHeartbeat to succeed, got: %v", err) + } + + req := <-requests + if req.Msg.GetError() != "" { + t.Errorf("expected heartbeat to have no error after initialization, got: %q", req.Msg.GetError()) + } +} + +func TestInitialiseAdapters_SetsInitializedFlag(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) + } + + if e.AreAdaptersInitialized() { + t.Fatal("expected adaptersInitialized to be false on new engine") + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + e.InitialiseAdapters(ctx, func(ctx context.Context) error { + return nil + }) + + if !e.AreAdaptersInitialized() { + t.Error("expected adaptersInitialized to be true after InitialiseAdapters success") + } +} + +func TestInitialiseAdapters_DoesNotSetFlagOnFailure(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()) + time.AfterFunc(500*time.Millisecond, cancel) + + done := make(chan struct{}) + go func() { + e.InitialiseAdapters(ctx, func(ctx context.Context) error { + return errors.New("always fails") + }) + close(done) + }() + + select { + case <-done: + case <-time.After(5 * time.Second): + t.Fatal("InitialiseAdapters did not return after context cancellation") + } + + if e.AreAdaptersInitialized() { + t.Error("expected adaptersInitialized to remain false when init always fails") + } +} diff --git a/go/discovery/heartbeat.go b/go/discovery/heartbeat.go index 0ae6a862..0a9ab3dd 100644 --- a/go/discovery/heartbeat.go +++ b/go/discovery/heartbeat.go @@ -62,6 +62,10 @@ func (e *Engine) SendHeartbeat(ctx context.Context, customErr error) error { allErrors = append(allErrors, initErr) } + if !e.AreAdaptersInitialized() { + allErrors = append(allErrors, errors.New("adapters not yet initialized")) + } + // Check adapter readiness (ReadinessCheck) - with timeout to prevent hanging if e.EngineConfig.HeartbeatOptions.ReadinessCheck != nil { // Add timeout for readiness checks to prevent hanging heartbeats diff --git a/go/discovery/heartbeat_test.go b/go/discovery/heartbeat_test.go index 94e55915..a2c1adc7 100644 --- a/go/discovery/heartbeat_test.go +++ b/go/discovery/heartbeat_test.go @@ -46,6 +46,7 @@ func TestHeartbeats(t *testing.T) { HeartbeatOptions: &heartbeatOptions, } e, _ := NewEngine(&ec) + e.MarkAdaptersInitialized() if err := e.AddAdapters( &TestAdapter{ diff --git a/sources/snapshot/cmd/root.go b/sources/snapshot/cmd/root.go index 0882ba22..e99791ea 100644 --- a/sources/snapshot/cmd/root.go +++ b/sources/snapshot/cmd/root.go @@ -78,7 +78,13 @@ with fixed data and deterministic re-runs of v6 investigations.`, e.SetInitError(initErr) sentry.CaptureException(initErr) } else { - e.StartSendingHeartbeats(ctx) + e.MarkAdaptersInitialized() + // Start() already launched the heartbeat loop, so StartSendingHeartbeats + // is a no-op here. Send an immediate heartbeat so the API server learns + // the source is healthy without waiting for the next tick. + if err := e.SendHeartbeat(ctx, nil); err != nil { + log.WithError(err).Warn("Failed to send post-init heartbeat") + } } <-ctx.Done() diff --git a/stdlib-source/cmd/root.go b/stdlib-source/cmd/root.go index d2e37d6d..7b4122a1 100644 --- a/stdlib-source/cmd/root.go +++ b/stdlib-source/cmd/root.go @@ -94,7 +94,13 @@ var rootCmd = &cobra.Command{ e.SetInitError(initErr) sentry.CaptureException(initErr) } else { - e.StartSendingHeartbeats(ctx) + e.MarkAdaptersInitialized() + // Start() already launched the heartbeat loop, so StartSendingHeartbeats + // is a no-op here. Send an immediate heartbeat so the API server learns + // the source is healthy without waiting for the next tick. + if err := e.SendHeartbeat(ctx, nil); err != nil { + log.WithError(err).Warn("Failed to send post-init heartbeat") + } } <-ctx.Done() From 49ec810d2ddb0bfa7d6325ca5926b0bd5ce82805 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:09:11 +0000 Subject: [PATCH 17/74] Add Azure Virtual Network Peerings Client and Adapter (#4016) image --- > [!NOTE] > **Medium Risk** > Adds a new Azure discovery adapter that enumerates and fetches virtual network peerings, increasing Azure API surface area and adapter initialization work. Risk is moderate due to new linked-item query generation (including cross-scope links) and potential changes in discovery graph/output, but it is read-only. > > **Overview** > Adds support for discovering Azure **Virtual Network Peerings** by introducing a `VirtualNetworkPeeringsClient` abstraction (with generated GoMock) and a new `NewNetworkVirtualNetworkPeering` searchable wrapper. > > Wires the new adapter into `manual/adapters.go` (including SDK client initialization and placeholder registration), adds linked-item queries from peerings to local/remote VNets and selective subnets with health derived from `ProvisioningState`, and updates shared resource-ID path key extraction to recognize `azure-network-virtual-network-peering`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f6c1b086b4c3086ab9962b558489d693ca631039. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 1593b32b0782e3b9337cdccd7aebc018a0d0c2cb --- .../virtual-network-peerings-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + .../manual/network-virtual-network-peering.go | 270 ++++++++++++++++ .../network-virtual-network-peering_test.go | 298 ++++++++++++++++++ .../mock_virtual_network_peerings_client.go | 72 +++++ sources/azure/shared/utils.go | 1 + 6 files changed, 686 insertions(+) create mode 100644 sources/azure/clients/virtual-network-peerings-client.go create mode 100644 sources/azure/manual/network-virtual-network-peering.go create mode 100644 sources/azure/manual/network-virtual-network-peering_test.go create mode 100644 sources/azure/shared/mocks/mock_virtual_network_peerings_client.go diff --git a/sources/azure/clients/virtual-network-peerings-client.go b/sources/azure/clients/virtual-network-peerings-client.go new file mode 100644 index 00000000..7bc7029d --- /dev/null +++ b/sources/azure/clients/virtual-network-peerings-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" +) + +//go:generate mockgen -destination=../shared/mocks/mock_virtual_network_peerings_client.go -package=mocks -source=virtual-network-peerings-client.go + +// VirtualNetworkPeeringsPager is a type alias for the generic Pager interface with virtual network peerings list response type. +type VirtualNetworkPeeringsPager = Pager[armnetwork.VirtualNetworkPeeringsClientListResponse] + +// VirtualNetworkPeeringsClient is an interface for interacting with Azure virtual network peerings. +type VirtualNetworkPeeringsClient interface { + Get(ctx context.Context, resourceGroupName string, virtualNetworkName string, peeringName string, options *armnetwork.VirtualNetworkPeeringsClientGetOptions) (armnetwork.VirtualNetworkPeeringsClientGetResponse, error) + NewListPager(resourceGroupName string, virtualNetworkName string, options *armnetwork.VirtualNetworkPeeringsClientListOptions) VirtualNetworkPeeringsPager +} + +type virtualNetworkPeeringsClientAdapter struct { + client *armnetwork.VirtualNetworkPeeringsClient +} + +func (a *virtualNetworkPeeringsClientAdapter) Get(ctx context.Context, resourceGroupName string, virtualNetworkName string, peeringName string, options *armnetwork.VirtualNetworkPeeringsClientGetOptions) (armnetwork.VirtualNetworkPeeringsClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, virtualNetworkName, peeringName, options) +} + +func (a *virtualNetworkPeeringsClientAdapter) NewListPager(resourceGroupName string, virtualNetworkName string, options *armnetwork.VirtualNetworkPeeringsClientListOptions) VirtualNetworkPeeringsPager { + return a.client.NewListPager(resourceGroupName, virtualNetworkName, options) +} + +// NewVirtualNetworkPeeringsClient creates a new VirtualNetworkPeeringsClient from the Azure SDK client. +func NewVirtualNetworkPeeringsClient(client *armnetwork.VirtualNetworkPeeringsClient) VirtualNetworkPeeringsClient { + return &virtualNetworkPeeringsClientAdapter{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 5a9c03b3..a7eac076 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -121,6 +121,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create subnets client: %w", err) } + virtualNetworkPeeringsClient, err := armnetwork.NewVirtualNetworkPeeringsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create virtual network peerings client: %w", err) + } + networkInterfacesClient, err := armnetwork.NewInterfacesClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create network interfaces client: %w", err) @@ -342,6 +347,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewSubnetsClient(subnetsClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewNetworkVirtualNetworkPeering( + clients.NewVirtualNetworkPeeringsClient(virtualNetworkPeeringsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewNetworkNetworkInterface( clients.NewNetworkInterfacesClient(networkInterfacesClient), resourceGroupScopes, @@ -518,6 +527,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewStorageEncryptionScope(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkVirtualNetwork(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkSubnet(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewNetworkVirtualNetworkPeering(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkNetworkInterface(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewSqlDatabase(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewSqlServerFirewallRule(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/network-virtual-network-peering.go b/sources/azure/manual/network-virtual-network-peering.go new file mode 100644 index 00000000..99368906 --- /dev/null +++ b/sources/azure/manual/network-virtual-network-peering.go @@ -0,0 +1,270 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +var NetworkVirtualNetworkPeeringLookupByUniqueAttr = shared.NewItemTypeLookup("uniqueAttr", azureshared.NetworkVirtualNetworkPeering) + +type networkVirtualNetworkPeeringWrapper struct { + client clients.VirtualNetworkPeeringsClient + + *azureshared.MultiResourceGroupBase +} + +// NewNetworkVirtualNetworkPeering creates a new networkVirtualNetworkPeeringWrapper instance (SearchableWrapper: child of virtual network). +func NewNetworkVirtualNetworkPeering(client clients.VirtualNetworkPeeringsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &networkVirtualNetworkPeeringWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + azureshared.NetworkVirtualNetworkPeering, + ), + } +} + +func (n networkVirtualNetworkPeeringWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: virtualNetworkName and peeringName", + Scope: scope, + ItemType: n.Type(), + } + } + virtualNetworkName := queryParts[0] + peeringName := queryParts[1] + if peeringName == "" { + return nil, azureshared.QueryError(errors.New("peering name cannot be empty"), scope, n.Type()) + } + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + resp, err := n.client.Get(ctx, rgScope.ResourceGroup, virtualNetworkName, peeringName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + return n.azureVirtualNetworkPeeringToSDPItem(&resp.VirtualNetworkPeering, virtualNetworkName, peeringName, scope) +} + +func (n networkVirtualNetworkPeeringWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + NetworkVirtualNetworkLookupByName, + NetworkVirtualNetworkPeeringLookupByUniqueAttr, + } +} + +func (n networkVirtualNetworkPeeringWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: virtualNetworkName", + Scope: scope, + ItemType: n.Type(), + } + } + virtualNetworkName := queryParts[0] + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + pager := n.client.NewListPager(rgScope.ResourceGroup, virtualNetworkName, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + for _, peering := range page.Value { + if peering == nil || peering.Name == nil { + continue + } + item, sdpErr := n.azureVirtualNetworkPeeringToSDPItem(peering, virtualNetworkName, *peering.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (n networkVirtualNetworkPeeringWrapper) 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: virtualNetworkName"), scope, n.Type())) + return + } + virtualNetworkName := queryParts[0] + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + pager := n.client.NewListPager(rgScope.ResourceGroup, virtualNetworkName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + for _, peering := range page.Value { + if peering == nil || peering.Name == nil { + continue + } + item, sdpErr := n.azureVirtualNetworkPeeringToSDPItem(peering, virtualNetworkName, *peering.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (n networkVirtualNetworkPeeringWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + NetworkVirtualNetworkLookupByName, + }, + } +} + +func (n networkVirtualNetworkPeeringWrapper) azureVirtualNetworkPeeringToSDPItem(peering *armnetwork.VirtualNetworkPeering, virtualNetworkName, peeringName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(peering, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(virtualNetworkName, peeringName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.NetworkVirtualNetworkPeering.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + } + + // Health status from ProvisioningState + if peering.Properties != nil && peering.Properties.ProvisioningState != nil { + switch *peering.Properties.ProvisioningState { + case armnetwork.ProvisioningStateSucceeded: + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case armnetwork.ProvisioningStateCreating, armnetwork.ProvisioningStateUpdating, armnetwork.ProvisioningStateDeleting: + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case armnetwork.ProvisioningStateFailed, armnetwork.ProvisioningStateCanceled: + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + } + } + + // Link to parent (local) Virtual Network + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkVirtualNetwork.String(), + Method: sdp.QueryMethod_GET, + Query: virtualNetworkName, + Scope: scope, + }, + }) + + // Link to remote Virtual Network and remote subnets (selective peering) + if peering.Properties != nil && peering.Properties.RemoteVirtualNetwork != nil && peering.Properties.RemoteVirtualNetwork.ID != nil { + remoteVNetID := *peering.Properties.RemoteVirtualNetwork.ID + remoteVNetName := azureshared.ExtractResourceName(remoteVNetID) + if remoteVNetName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(remoteVNetID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkVirtualNetwork.String(), + Method: sdp.QueryMethod_GET, + Query: remoteVNetName, + Scope: linkedScope, + }, + }) + // Link to remote subnets (selective subnet peering) + if peering.Properties.RemoteSubnetNames != nil { + for _, name := range peering.Properties.RemoteSubnetNames { + if name != nil && *name != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkSubnet.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(remoteVNetName, *name), + Scope: linkedScope, + }, + }) + } + } + } + } + } + + // Link to local subnets (selective subnet peering) + if peering.Properties != nil && peering.Properties.LocalSubnetNames != nil { + for _, name := range peering.Properties.LocalSubnetNames { + if name != nil && *name != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkSubnet.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(virtualNetworkName, *name), + Scope: scope, + }, + }) + } + } + } + + return sdpItem, nil +} + +func (n networkVirtualNetworkPeeringWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.NetworkVirtualNetwork, + azureshared.NetworkSubnet, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network_peering +func (n networkVirtualNetworkPeeringWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_virtual_network_peering.id", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions-reference#microsoftnetwork +func (n networkVirtualNetworkPeeringWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/read", + } +} + +func (n networkVirtualNetworkPeeringWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/network-virtual-network-peering_test.go b/sources/azure/manual/network-virtual-network-peering_test.go new file mode 100644 index 00000000..136e9184 --- /dev/null +++ b/sources/azure/manual/network-virtual-network-peering_test.go @@ -0,0 +1,298 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + sdp "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" +) + +type mockVirtualNetworkPeeringsPager struct { + pages []armnetwork.VirtualNetworkPeeringsClientListResponse + index int +} + +func (m *mockVirtualNetworkPeeringsPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockVirtualNetworkPeeringsPager) NextPage(ctx context.Context) (armnetwork.VirtualNetworkPeeringsClientListResponse, error) { + if m.index >= len(m.pages) { + return armnetwork.VirtualNetworkPeeringsClientListResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type errorVirtualNetworkPeeringsPager struct{} + +func (e *errorVirtualNetworkPeeringsPager) More() bool { + return true +} + +func (e *errorVirtualNetworkPeeringsPager) NextPage(ctx context.Context) (armnetwork.VirtualNetworkPeeringsClientListResponse, error) { + return armnetwork.VirtualNetworkPeeringsClientListResponse{}, errors.New("pager error") +} + +type testVirtualNetworkPeeringsClient struct { + *mocks.MockVirtualNetworkPeeringsClient + pager clients.VirtualNetworkPeeringsPager +} + +func (t *testVirtualNetworkPeeringsClient) NewListPager(resourceGroupName, virtualNetworkName string, options *armnetwork.VirtualNetworkPeeringsClientListOptions) clients.VirtualNetworkPeeringsPager { + return t.pager +} + +func TestNetworkVirtualNetworkPeering(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + virtualNetworkName := "test-vnet" + peeringName := "test-peering" + + t.Run("Get", func(t *testing.T) { + peering := createAzureVirtualNetworkPeering(peeringName, virtualNetworkName) + + mockClient := mocks.NewMockVirtualNetworkPeeringsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, virtualNetworkName, peeringName, nil).Return( + armnetwork.VirtualNetworkPeeringsClientGetResponse{ + VirtualNetworkPeering: *peering, + }, nil) + + testClient := &testVirtualNetworkPeeringsClient{MockVirtualNetworkPeeringsClient: mockClient} + wrapper := manual.NewNetworkVirtualNetworkPeering(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(virtualNetworkName, peeringName) + 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.NetworkVirtualNetworkPeering.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkVirtualNetworkPeering, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != shared.CompositeLookupKey(virtualNetworkName, peeringName) { + t.Errorf("Expected unique attribute value %s, got %s", shared.CompositeLookupKey(virtualNetworkName, peeringName), sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.NetworkVirtualNetwork.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: virtualNetworkName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_EmptyPeeringName", func(t *testing.T) { + mockClient := mocks.NewMockVirtualNetworkPeeringsClient(ctrl) + testClient := &testVirtualNetworkPeeringsClient{MockVirtualNetworkPeeringsClient: mockClient} + + wrapper := manual.NewNetworkVirtualNetworkPeering(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(virtualNetworkName, "") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when peering name is empty, but got nil") + } + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockVirtualNetworkPeeringsClient(ctrl) + testClient := &testVirtualNetworkPeeringsClient{MockVirtualNetworkPeeringsClient: mockClient} + + wrapper := manual.NewNetworkVirtualNetworkPeering(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], virtualNetworkName, true) + if qErr == nil { + t.Error("Expected error when providing insufficient query parts, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + peering1 := createAzureVirtualNetworkPeering("peering-1", virtualNetworkName) + peering2 := createAzureVirtualNetworkPeering("peering-2", virtualNetworkName) + + mockClient := mocks.NewMockVirtualNetworkPeeringsClient(ctrl) + mockPager := &mockVirtualNetworkPeeringsPager{ + pages: []armnetwork.VirtualNetworkPeeringsClientListResponse{ + { + VirtualNetworkPeeringListResult: armnetwork.VirtualNetworkPeeringListResult{ + Value: []*armnetwork.VirtualNetworkPeering{peering1, peering2}, + }, + }, + }, + } + + testClient := &testVirtualNetworkPeeringsClient{ + MockVirtualNetworkPeeringsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewNetworkVirtualNetworkPeering(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, wrapper.Scopes()[0], virtualNetworkName, 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.Fatalf("Expected no validation error, got: %v", err) + } + if item.GetType() != azureshared.NetworkVirtualNetworkPeering.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkVirtualNetworkPeering, item.GetType()) + } + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockVirtualNetworkPeeringsClient(ctrl) + testClient := &testVirtualNetworkPeeringsClient{MockVirtualNetworkPeeringsClient: mockClient} + + wrapper := manual.NewNetworkVirtualNetworkPeering(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("Search_PeeringWithNilName", func(t *testing.T) { + validPeering := createAzureVirtualNetworkPeering("valid-peering", virtualNetworkName) + + mockClient := mocks.NewMockVirtualNetworkPeeringsClient(ctrl) + mockPager := &mockVirtualNetworkPeeringsPager{ + pages: []armnetwork.VirtualNetworkPeeringsClientListResponse{ + { + VirtualNetworkPeeringListResult: armnetwork.VirtualNetworkPeeringListResult{ + Value: []*armnetwork.VirtualNetworkPeering{ + {Name: nil, ID: strPtr("/some/id")}, + validPeering, + }, + }, + }, + }, + } + + testClient := &testVirtualNetworkPeeringsClient{ + MockVirtualNetworkPeeringsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewNetworkVirtualNetworkPeering(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable := adapter.(discovery.SearchableAdapter) + sdpItems, err := searchable.Search(ctx, wrapper.Scopes()[0], virtualNetworkName, 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)) + } + if sdpItems[0].UniqueAttributeValue() != shared.CompositeLookupKey(virtualNetworkName, "valid-peering") { + t.Errorf("Expected unique value %s, got %s", shared.CompositeLookupKey(virtualNetworkName, "valid-peering"), sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("peering not found") + + mockClient := mocks.NewMockVirtualNetworkPeeringsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, virtualNetworkName, "nonexistent-peering", nil).Return( + armnetwork.VirtualNetworkPeeringsClientGetResponse{}, expectedErr) + + testClient := &testVirtualNetworkPeeringsClient{MockVirtualNetworkPeeringsClient: mockClient} + wrapper := manual.NewNetworkVirtualNetworkPeering(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(virtualNetworkName, "nonexistent-peering") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent peering, but got nil") + } + }) + + t.Run("ErrorHandling_Search", func(t *testing.T) { + mockClient := mocks.NewMockVirtualNetworkPeeringsClient(ctrl) + testClient := &testVirtualNetworkPeeringsClient{ + MockVirtualNetworkPeeringsClient: mockClient, + pager: &errorVirtualNetworkPeeringsPager{}, + } + + wrapper := manual.NewNetworkVirtualNetworkPeering(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable := adapter.(discovery.SearchableAdapter) + _, err := searchable.Search(ctx, wrapper.Scopes()[0], virtualNetworkName, true) + if err == nil { + t.Error("Expected error from pager when NextPage returns an error, but got nil") + } + }) +} + +func createAzureVirtualNetworkPeering(peeringName, vnetName string) *armnetwork.VirtualNetworkPeering { + idStr := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/" + vnetName + "/virtualNetworkPeerings/" + peeringName + typeStr := "Microsoft.Network/virtualNetworks/virtualNetworkPeerings" + provisioningState := armnetwork.ProvisioningStateSucceeded + return &armnetwork.VirtualNetworkPeering{ + ID: &idStr, + Name: &peeringName, + Type: &typeStr, + Properties: &armnetwork.VirtualNetworkPeeringPropertiesFormat{ + ProvisioningState: &provisioningState, + }, + } +} + +func strPtr(s string) *string { + return &s +} diff --git a/sources/azure/shared/mocks/mock_virtual_network_peerings_client.go b/sources/azure/shared/mocks/mock_virtual_network_peerings_client.go new file mode 100644 index 00000000..8498857b --- /dev/null +++ b/sources/azure/shared/mocks/mock_virtual_network_peerings_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: virtual-network-peerings-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_virtual_network_peerings_client.go -package=mocks -source=virtual-network-peerings-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockVirtualNetworkPeeringsClient is a mock of VirtualNetworkPeeringsClient interface. +type MockVirtualNetworkPeeringsClient struct { + ctrl *gomock.Controller + recorder *MockVirtualNetworkPeeringsClientMockRecorder + isgomock struct{} +} + +// MockVirtualNetworkPeeringsClientMockRecorder is the mock recorder for MockVirtualNetworkPeeringsClient. +type MockVirtualNetworkPeeringsClientMockRecorder struct { + mock *MockVirtualNetworkPeeringsClient +} + +// NewMockVirtualNetworkPeeringsClient creates a new mock instance. +func NewMockVirtualNetworkPeeringsClient(ctrl *gomock.Controller) *MockVirtualNetworkPeeringsClient { + mock := &MockVirtualNetworkPeeringsClient{ctrl: ctrl} + mock.recorder = &MockVirtualNetworkPeeringsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockVirtualNetworkPeeringsClient) EXPECT() *MockVirtualNetworkPeeringsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockVirtualNetworkPeeringsClient) Get(ctx context.Context, resourceGroupName, virtualNetworkName, peeringName string, options *armnetwork.VirtualNetworkPeeringsClientGetOptions) (armnetwork.VirtualNetworkPeeringsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, virtualNetworkName, peeringName, options) + ret0, _ := ret[0].(armnetwork.VirtualNetworkPeeringsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockVirtualNetworkPeeringsClientMockRecorder) Get(ctx, resourceGroupName, virtualNetworkName, peeringName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockVirtualNetworkPeeringsClient)(nil).Get), ctx, resourceGroupName, virtualNetworkName, peeringName, options) +} + +// NewListPager mocks base method. +func (m *MockVirtualNetworkPeeringsClient) NewListPager(resourceGroupName, virtualNetworkName string, options *armnetwork.VirtualNetworkPeeringsClientListOptions) clients.VirtualNetworkPeeringsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListPager", resourceGroupName, virtualNetworkName, options) + ret0, _ := ret[0].(clients.VirtualNetworkPeeringsPager) + return ret0 +} + +// NewListPager indicates an expected call of NewListPager. +func (mr *MockVirtualNetworkPeeringsClientMockRecorder) NewListPager(resourceGroupName, virtualNetworkName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListPager", reflect.TypeOf((*MockVirtualNetworkPeeringsClient)(nil).NewListPager), resourceGroupName, virtualNetworkName, options) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 94438104..f6d64b00 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -34,6 +34,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-compute-gallery-application": {"galleries", "applications"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{applicationName}", "azure-compute-gallery-image": {"galleries", "images"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/images/{imageName}", "azure-network-subnet": {"virtualNetworks", "subnets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName}", + "azure-network-virtual-network-peering": {"virtualNetworks", "virtualNetworkPeerings"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/virtualNetworkPeerings/{peeringName}", } if keys, ok := pathKeysMap[resourceType]; ok { From a7101df0b1adac040e17011285da0fd332ee8ed8 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:46:22 +0000 Subject: [PATCH 18/74] Eng 2816 create networkroute adapter (#4020) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image > [!NOTE] > **Medium Risk** > Adds a new Azure discovery adapter and wires it into adapter initialization, increasing API surface and potential for mis-scoped queries or paging-related issues. Risk is mitigated by unit tests and the change being additive (no existing adapters’ logic is modified). > > **Overview** > Adds first-class discovery support for Azure route table routes via a new `NetworkRoute` searchable wrapper, including `Get` and paged list/search (plus streaming) and SDP item mapping (health + links to parent route table and `stdlib.NetworkIP`). > > Introduces a thin `RoutesClient` wrapper around the Azure SDK (with generated mocks), wires the new adapter into `manual/adapters.go` initialization, and updates Azure resource ID path-key extraction to recognize `azure-network-route`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c022fe6eba559588515ae230931d1bc9bf269747. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 8ba151b02117da4308c0ba0521122a1ee380001c --- sources/azure/clients/routes-client.go | 35 ++ sources/azure/manual/adapters.go | 9 + sources/azure/manual/network-route.go | 224 +++++++++++++ sources/azure/manual/network-route_test.go | 306 ++++++++++++++++++ .../azure/shared/mocks/mock_routes_client.go | 72 +++++ sources/azure/shared/utils.go | 1 + 6 files changed, 647 insertions(+) create mode 100644 sources/azure/clients/routes-client.go create mode 100644 sources/azure/manual/network-route.go create mode 100644 sources/azure/manual/network-route_test.go create mode 100644 sources/azure/shared/mocks/mock_routes_client.go diff --git a/sources/azure/clients/routes-client.go b/sources/azure/clients/routes-client.go new file mode 100644 index 00000000..7a23239d --- /dev/null +++ b/sources/azure/clients/routes-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" +) + +//go:generate mockgen -destination=../shared/mocks/mock_routes_client.go -package=mocks -source=routes-client.go + +// RoutesPager is a type alias for the generic Pager interface with routes list response type. +type RoutesPager = Pager[armnetwork.RoutesClientListResponse] + +// RoutesClient is an interface for interacting with Azure routes (child of route table). +type RoutesClient interface { + Get(ctx context.Context, resourceGroupName string, routeTableName string, routeName string, options *armnetwork.RoutesClientGetOptions) (armnetwork.RoutesClientGetResponse, error) + NewListPager(resourceGroupName string, routeTableName string, options *armnetwork.RoutesClientListOptions) RoutesPager +} + +type routesClient struct { + client *armnetwork.RoutesClient +} + +func (a *routesClient) Get(ctx context.Context, resourceGroupName string, routeTableName string, routeName string, options *armnetwork.RoutesClientGetOptions) (armnetwork.RoutesClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, routeTableName, routeName, options) +} + +func (a *routesClient) NewListPager(resourceGroupName string, routeTableName string, options *armnetwork.RoutesClientListOptions) RoutesPager { + return a.client.NewListPager(resourceGroupName, routeTableName, options) +} + +// NewRoutesClient creates a new RoutesClient from the Azure SDK client. +func NewRoutesClient(client *armnetwork.RoutesClient) RoutesClient { + return &routesClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index a7eac076..2e5980a8 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -195,6 +195,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create route tables client: %w", err) } + routesClient, err := armnetwork.NewRoutesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create routes client: %w", err) + } + applicationGatewaysClient, err := armnetwork.NewApplicationGatewaysClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create application gateways client: %w", err) @@ -419,6 +424,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewRouteTablesClient(routeTablesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewNetworkRoute( + clients.NewRoutesClient(routesClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewNetworkApplicationGateway( clients.NewApplicationGatewaysClient(applicationGatewaysClient), resourceGroupScopes, diff --git a/sources/azure/manual/network-route.go b/sources/azure/manual/network-route.go new file mode 100644 index 00000000..a7f66a12 --- /dev/null +++ b/sources/azure/manual/network-route.go @@ -0,0 +1,224 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "github.com/overmindtech/cli/go/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 NetworkRouteLookupByUniqueAttr = shared.NewItemTypeLookup("uniqueAttr", azureshared.NetworkRoute) + +type networkRouteWrapper struct { + client clients.RoutesClient + *azureshared.MultiResourceGroupBase +} + +// NewNetworkRoute creates a new networkRouteWrapper instance (SearchableWrapper: child of route table). +func NewNetworkRoute(client clients.RoutesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &networkRouteWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + azureshared.NetworkRoute, + ), + } +} + +func (n networkRouteWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: routeTableName and routeName", + Scope: scope, + ItemType: n.Type(), + } + } + routeTableName := queryParts[0] + routeName := queryParts[1] + if routeName == "" { + return nil, azureshared.QueryError(errors.New("route name cannot be empty"), scope, n.Type()) + } + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + resp, err := n.client.Get(ctx, rgScope.ResourceGroup, routeTableName, routeName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + return n.azureRouteToSDPItem(&resp.Route, routeTableName, routeName, scope) +} + +func (n networkRouteWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + NetworkRouteTableLookupByName, + NetworkRouteLookupByUniqueAttr, + } +} + +func (n networkRouteWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: routeTableName", + Scope: scope, + ItemType: n.Type(), + } + } + routeTableName := queryParts[0] + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + pager := n.client.NewListPager(rgScope.ResourceGroup, routeTableName, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + for _, route := range page.Value { + if route == nil || route.Name == nil { + continue + } + item, sdpErr := n.azureRouteToSDPItem(route, routeTableName, *route.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (n networkRouteWrapper) 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: routeTableName"), scope, n.Type())) + return + } + routeTableName := queryParts[0] + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + pager := n.client.NewListPager(rgScope.ResourceGroup, routeTableName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + for _, route := range page.Value { + if route == nil || route.Name == nil { + continue + } + item, sdpErr := n.azureRouteToSDPItem(route, routeTableName, *route.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (n networkRouteWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + {NetworkRouteTableLookupByName}, + } +} + +func (n networkRouteWrapper) azureRouteToSDPItem(route *armnetwork.Route, routeTableName, routeName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(route, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(routeTableName, routeName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.NetworkRoute.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + } + + // Health status from ProvisioningState + if route.Properties != nil && route.Properties.ProvisioningState != nil { + switch *route.Properties.ProvisioningState { + case armnetwork.ProvisioningStateSucceeded: + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case armnetwork.ProvisioningStateCreating, armnetwork.ProvisioningStateUpdating, armnetwork.ProvisioningStateDeleting: + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case armnetwork.ProvisioningStateFailed, armnetwork.ProvisioningStateCanceled: + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + } + } + + // Link to parent Route Table + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkRouteTable.String(), + Method: sdp.QueryMethod_GET, + Query: routeTableName, + Scope: scope, + }, + }) + + // Link to NextHopIPAddress (IP address to stdlib) + if route.Properties != nil && route.Properties.NextHopIPAddress != nil && *route.Properties.NextHopIPAddress != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *route.Properties.NextHopIPAddress, + Scope: "global", + }, + }) + } + + return sdpItem, nil +} + +func (n networkRouteWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.NetworkRouteTable, + stdlib.NetworkIP, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/route +func (n networkRouteWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + {TerraformMethod: sdp.QueryMethod_SEARCH, TerraformQueryMap: "azurerm_route.id"}, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions-reference#microsoftnetwork +func (n networkRouteWrapper) IAMPermissions() []string { + return []string{"Microsoft.Network/routeTables/routes/read"} +} + +func (n networkRouteWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/network-route_test.go b/sources/azure/manual/network-route_test.go new file mode 100644 index 00000000..a2e25f34 --- /dev/null +++ b/sources/azure/manual/network-route_test.go @@ -0,0 +1,306 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + sdp "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" +) + +type mockRoutesPager struct { + pages []armnetwork.RoutesClientListResponse + index int +} + +func (m *mockRoutesPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockRoutesPager) NextPage(ctx context.Context) (armnetwork.RoutesClientListResponse, error) { + if m.index >= len(m.pages) { + return armnetwork.RoutesClientListResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type errorRoutesPager struct{} + +func (e *errorRoutesPager) More() bool { + return true +} + +func (e *errorRoutesPager) NextPage(ctx context.Context) (armnetwork.RoutesClientListResponse, error) { + return armnetwork.RoutesClientListResponse{}, errors.New("pager error") +} + +type testRoutesClient struct { + *mocks.MockRoutesClient + pager clients.RoutesPager +} + +func (t *testRoutesClient) NewListPager(resourceGroupName, routeTableName string, options *armnetwork.RoutesClientListOptions) clients.RoutesPager { + return t.pager +} + +func TestNetworkRoute(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + routeTableName := "test-route-table" + routeName := "test-route" + + t.Run("Get", func(t *testing.T) { + route := createAzureRoute(routeName, routeTableName) + + mockClient := mocks.NewMockRoutesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, routeTableName, routeName, nil).Return( + armnetwork.RoutesClientGetResponse{ + Route: *route, + }, nil) + + testClient := &testRoutesClient{MockRoutesClient: mockClient} + wrapper := manual.NewNetworkRoute(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(routeTableName, routeName) + 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.NetworkRoute.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkRoute, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != shared.CompositeLookupKey(routeTableName, routeName) { + t.Errorf("Expected unique attribute value %s, got %s", shared.CompositeLookupKey(routeTableName, routeName), sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.NetworkRouteTable.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: routeTableName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, + { + ExpectedType: stdlib.NetworkIP.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "10.0.0.1", + ExpectedScope: "global", + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_EmptyRouteName", func(t *testing.T) { + mockClient := mocks.NewMockRoutesClient(ctrl) + testClient := &testRoutesClient{MockRoutesClient: mockClient} + + wrapper := manual.NewNetworkRoute(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(routeTableName, "") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when route name is empty, but got nil") + } + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockRoutesClient(ctrl) + testClient := &testRoutesClient{MockRoutesClient: mockClient} + + wrapper := manual.NewNetworkRoute(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], routeTableName, true) + if qErr == nil { + t.Error("Expected error when providing insufficient query parts, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + route1 := createAzureRoute("route-1", routeTableName) + route2 := createAzureRoute("route-2", routeTableName) + + mockClient := mocks.NewMockRoutesClient(ctrl) + mockPager := &mockRoutesPager{ + pages: []armnetwork.RoutesClientListResponse{ + { + RouteListResult: armnetwork.RouteListResult{ + Value: []*armnetwork.Route{route1, route2}, + }, + }, + }, + } + + testClient := &testRoutesClient{ + MockRoutesClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewNetworkRoute(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, wrapper.Scopes()[0], routeTableName, 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.Fatalf("Expected no validation error, got: %v", err) + } + if item.GetType() != azureshared.NetworkRoute.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkRoute, item.GetType()) + } + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockRoutesClient(ctrl) + testClient := &testRoutesClient{MockRoutesClient: mockClient} + + wrapper := manual.NewNetworkRoute(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("Search_RouteWithNilName", func(t *testing.T) { + validRoute := createAzureRoute("valid-route", routeTableName) + + mockClient := mocks.NewMockRoutesClient(ctrl) + mockPager := &mockRoutesPager{ + pages: []armnetwork.RoutesClientListResponse{ + { + RouteListResult: armnetwork.RouteListResult{ + Value: []*armnetwork.Route{ + {Name: nil, ID: strPtr("/some/id")}, + validRoute, + }, + }, + }, + }, + } + + testClient := &testRoutesClient{ + MockRoutesClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewNetworkRoute(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable := adapter.(discovery.SearchableAdapter) + sdpItems, err := searchable.Search(ctx, wrapper.Scopes()[0], routeTableName, 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)) + } + if sdpItems[0].UniqueAttributeValue() != shared.CompositeLookupKey(routeTableName, "valid-route") { + t.Errorf("Expected unique value %s, got %s", shared.CompositeLookupKey(routeTableName, "valid-route"), sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("route not found") + + mockClient := mocks.NewMockRoutesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, routeTableName, "nonexistent-route", nil).Return( + armnetwork.RoutesClientGetResponse{}, expectedErr) + + testClient := &testRoutesClient{MockRoutesClient: mockClient} + wrapper := manual.NewNetworkRoute(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(routeTableName, "nonexistent-route") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent route, but got nil") + } + }) + + t.Run("ErrorHandling_Search", func(t *testing.T) { + mockClient := mocks.NewMockRoutesClient(ctrl) + testClient := &testRoutesClient{ + MockRoutesClient: mockClient, + pager: &errorRoutesPager{}, + } + + wrapper := manual.NewNetworkRoute(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable := adapter.(discovery.SearchableAdapter) + _, err := searchable.Search(ctx, wrapper.Scopes()[0], routeTableName, true) + if err == nil { + t.Error("Expected error from pager when NextPage returns an error, but got nil") + } + }) +} + +func createAzureRoute(routeName, routeTableName string) *armnetwork.Route { + idStr := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/routeTables/" + routeTableName + "/routes/" + routeName + typeStr := "Microsoft.Network/routeTables/routes" + provisioningState := armnetwork.ProvisioningStateSucceeded + nextHopIP := "10.0.0.1" + nextHopType := armnetwork.RouteNextHopTypeVnetLocal + return &armnetwork.Route{ + ID: &idStr, + Name: &routeName, + Type: &typeStr, + Properties: &armnetwork.RoutePropertiesFormat{ + ProvisioningState: &provisioningState, + NextHopIPAddress: &nextHopIP, + AddressPrefix: strPtr("10.0.0.0/24"), + NextHopType: &nextHopType, + }, + } +} diff --git a/sources/azure/shared/mocks/mock_routes_client.go b/sources/azure/shared/mocks/mock_routes_client.go new file mode 100644 index 00000000..fd6f67bb --- /dev/null +++ b/sources/azure/shared/mocks/mock_routes_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: routes-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_routes_client.go -package=mocks -source=routes-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockRoutesClient is a mock of RoutesClient interface. +type MockRoutesClient struct { + ctrl *gomock.Controller + recorder *MockRoutesClientMockRecorder + isgomock struct{} +} + +// MockRoutesClientMockRecorder is the mock recorder for MockRoutesClient. +type MockRoutesClientMockRecorder struct { + mock *MockRoutesClient +} + +// NewMockRoutesClient creates a new mock instance. +func NewMockRoutesClient(ctrl *gomock.Controller) *MockRoutesClient { + mock := &MockRoutesClient{ctrl: ctrl} + mock.recorder = &MockRoutesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRoutesClient) EXPECT() *MockRoutesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockRoutesClient) Get(ctx context.Context, resourceGroupName, routeTableName, routeName string, options *armnetwork.RoutesClientGetOptions) (armnetwork.RoutesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, routeTableName, routeName, options) + ret0, _ := ret[0].(armnetwork.RoutesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockRoutesClientMockRecorder) Get(ctx, resourceGroupName, routeTableName, routeName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRoutesClient)(nil).Get), ctx, resourceGroupName, routeTableName, routeName, options) +} + +// NewListPager mocks base method. +func (m *MockRoutesClient) NewListPager(resourceGroupName, routeTableName string, options *armnetwork.RoutesClientListOptions) clients.RoutesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListPager", resourceGroupName, routeTableName, options) + ret0, _ := ret[0].(clients.RoutesPager) + return ret0 +} + +// NewListPager indicates an expected call of NewListPager. +func (mr *MockRoutesClientMockRecorder) NewListPager(resourceGroupName, routeTableName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListPager", reflect.TypeOf((*MockRoutesClient)(nil).NewListPager), resourceGroupName, routeTableName, options) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index f6d64b00..9378b5ef 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -35,6 +35,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-compute-gallery-image": {"galleries", "images"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/images/{imageName}", "azure-network-subnet": {"virtualNetworks", "subnets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName}", "azure-network-virtual-network-peering": {"virtualNetworks", "virtualNetworkPeerings"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/virtualNetworkPeerings/{peeringName}", + "azure-network-route": {"routeTables", "routes"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/routeTables/{routeTableName}/routes/{routeName}", } if keys, ok := pathKeysMap[resourceType]; ok { From 84dde2f835ef877ffa326847796b6ce551606bc2 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:10:07 +0000 Subject: [PATCH 19/74] Add Azure Security Rules Client and Adapter (#4021) image > [!NOTE] > **Medium Risk** > Introduces a new Azure discovery adapter that lists/gets NSG security rules and wires it into adapter initialization, increasing Azure API surface area and calls during discovery. Changes are isolated to network inventory but could affect discovery performance/permissions if mis-scoped. > > **Overview** > Adds first-class discovery for **Azure NSG `securityRules`** by introducing a `SecurityRulesClient` wrapper and a new `NetworkSecurityRule` searchable adapter (Get/Search/SearchStream) that models rules with a composite unique key (`nsgName + ruleName`). > > Wires the new adapter into `manual/adapters.go` (including placeholder registration) and extends resource-ID path parsing (`shared/utils.go`) to support `azure-network-security-rule`; includes generated GoMock client and comprehensive unit tests for get/search and error cases. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 617bf71d5460099e58869e0fec78509f69241d4a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: eb5f6cfefc8b0d03081e4b47436b926694094cc2 --- .../azure/clients/security-rules-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + sources/azure/manual/network-security-rule.go | 272 ++++++++++++++++ .../manual/network-security-rule_test.go | 300 ++++++++++++++++++ .../mocks/mock_security_rules_client.go | 72 +++++ sources/azure/shared/utils.go | 1 + 6 files changed, 690 insertions(+) create mode 100644 sources/azure/clients/security-rules-client.go create mode 100644 sources/azure/manual/network-security-rule.go create mode 100644 sources/azure/manual/network-security-rule_test.go create mode 100644 sources/azure/shared/mocks/mock_security_rules_client.go diff --git a/sources/azure/clients/security-rules-client.go b/sources/azure/clients/security-rules-client.go new file mode 100644 index 00000000..1a68f0d2 --- /dev/null +++ b/sources/azure/clients/security-rules-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" +) + +//go:generate mockgen -destination=../shared/mocks/mock_security_rules_client.go -package=mocks -source=security-rules-client.go + +// SecurityRulesPager is a type alias for the generic Pager interface with security rules list response type. +type SecurityRulesPager = Pager[armnetwork.SecurityRulesClientListResponse] + +// SecurityRulesClient is an interface for interacting with Azure NSG security rules (child of network security group). +type SecurityRulesClient interface { + Get(ctx context.Context, resourceGroupName string, networkSecurityGroupName string, securityRuleName string, options *armnetwork.SecurityRulesClientGetOptions) (armnetwork.SecurityRulesClientGetResponse, error) + NewListPager(resourceGroupName string, networkSecurityGroupName string, options *armnetwork.SecurityRulesClientListOptions) SecurityRulesPager +} + +type securityRulesClient struct { + client *armnetwork.SecurityRulesClient +} + +func (a *securityRulesClient) Get(ctx context.Context, resourceGroupName string, networkSecurityGroupName string, securityRuleName string, options *armnetwork.SecurityRulesClientGetOptions) (armnetwork.SecurityRulesClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, networkSecurityGroupName, securityRuleName, options) +} + +func (a *securityRulesClient) NewListPager(resourceGroupName string, networkSecurityGroupName string, options *armnetwork.SecurityRulesClientListOptions) SecurityRulesPager { + return a.client.NewListPager(resourceGroupName, networkSecurityGroupName, options) +} + +// NewSecurityRulesClient creates a new SecurityRulesClient from the Azure SDK client. +func NewSecurityRulesClient(client *armnetwork.SecurityRulesClient) SecurityRulesClient { + return &securityRulesClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 2e5980a8..d18a30b1 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -200,6 +200,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create routes client: %w", err) } + securityRulesClient, err := armnetwork.NewSecurityRulesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create security rules client: %w", err) + } + applicationGatewaysClient, err := armnetwork.NewApplicationGatewaysClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create application gateways client: %w", err) @@ -428,6 +433,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewRoutesClient(routesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewNetworkSecurityRule( + clients.NewSecurityRulesClient(securityRulesClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewNetworkApplicationGateway( clients.NewApplicationGatewaysClient(applicationGatewaysClient), resourceGroupScopes, @@ -552,6 +561,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewComputeAvailabilitySet(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeDisk(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkNetworkSecurityGroup(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewNetworkSecurityRule(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkRouteTable(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkApplicationGateway(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewSqlServer(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/network-security-rule.go b/sources/azure/manual/network-security-rule.go new file mode 100644 index 00000000..fcaf5cc1 --- /dev/null +++ b/sources/azure/manual/network-security-rule.go @@ -0,0 +1,272 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "github.com/overmindtech/cli/go/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 NetworkSecurityRuleLookupByUniqueAttr = shared.NewItemTypeLookup("uniqueAttr", azureshared.NetworkSecurityRule) + +type networkSecurityRuleWrapper struct { + client clients.SecurityRulesClient + *azureshared.MultiResourceGroupBase +} + +// NewNetworkSecurityRule creates a new networkSecurityRuleWrapper instance (SearchableWrapper: child of network security group). +func NewNetworkSecurityRule(client clients.SecurityRulesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &networkSecurityRuleWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + azureshared.NetworkSecurityRule, + ), + } +} + +func (n networkSecurityRuleWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: networkSecurityGroupName and securityRuleName", + Scope: scope, + ItemType: n.Type(), + } + } + nsgName := queryParts[0] + ruleName := queryParts[1] + if ruleName == "" { + return nil, azureshared.QueryError(errors.New("security rule name cannot be empty"), scope, n.Type()) + } + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + resp, err := n.client.Get(ctx, rgScope.ResourceGroup, nsgName, ruleName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + return n.azureSecurityRuleToSDPItem(&resp.SecurityRule, nsgName, ruleName, scope) +} + +func (n networkSecurityRuleWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + NetworkNetworkSecurityGroupLookupByName, + NetworkSecurityRuleLookupByUniqueAttr, + } +} + +func (n networkSecurityRuleWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: networkSecurityGroupName", + Scope: scope, + ItemType: n.Type(), + } + } + nsgName := queryParts[0] + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + pager := n.client.NewListPager(rgScope.ResourceGroup, nsgName, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + for _, rule := range page.Value { + if rule == nil || rule.Name == nil { + continue + } + item, sdpErr := n.azureSecurityRuleToSDPItem(rule, nsgName, *rule.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (n networkSecurityRuleWrapper) 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: networkSecurityGroupName"), scope, n.Type())) + return + } + nsgName := queryParts[0] + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + pager := n.client.NewListPager(rgScope.ResourceGroup, nsgName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + for _, rule := range page.Value { + if rule == nil || rule.Name == nil { + continue + } + item, sdpErr := n.azureSecurityRuleToSDPItem(rule, nsgName, *rule.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (n networkSecurityRuleWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + {NetworkNetworkSecurityGroupLookupByName}, + } +} + +func (n networkSecurityRuleWrapper) azureSecurityRuleToSDPItem(rule *armnetwork.SecurityRule, nsgName, ruleName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(rule, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(nsgName, ruleName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.NetworkSecurityRule.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + } + + // Link to parent Network Security Group + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkNetworkSecurityGroup.String(), + Method: sdp.QueryMethod_GET, + Query: nsgName, + Scope: scope, + }, + }) + + if rule.Properties != nil { + // Link to SourceApplicationSecurityGroups + if rule.Properties.SourceApplicationSecurityGroups != nil { + for _, asgRef := range rule.Properties.SourceApplicationSecurityGroups { + if asgRef != nil && asgRef.ID != nil { + asgName := azureshared.ExtractResourceName(*asgRef.ID) + if asgName != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(*asgRef.ID); extractedScope != "" { + linkScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkApplicationSecurityGroup.String(), + Method: sdp.QueryMethod_GET, + Query: asgName, + Scope: linkScope, + }, + }) + } + } + } + } + + // Link to DestinationApplicationSecurityGroups + if rule.Properties.DestinationApplicationSecurityGroups != nil { + for _, asgRef := range rule.Properties.DestinationApplicationSecurityGroups { + if asgRef != nil && asgRef.ID != nil { + asgName := azureshared.ExtractResourceName(*asgRef.ID) + if asgName != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(*asgRef.ID); extractedScope != "" { + linkScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkApplicationSecurityGroup.String(), + Method: sdp.QueryMethod_GET, + Query: asgName, + Scope: linkScope, + }, + }) + } + } + } + } + + // Link to stdlib.NetworkIP for source/destination address prefixes when they are IPs or CIDRs + if rule.Properties.SourceAddressPrefix != nil { + appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *rule.Properties.SourceAddressPrefix) + } + for _, p := range rule.Properties.SourceAddressPrefixes { + if p != nil { + appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *p) + } + } + if rule.Properties.DestinationAddressPrefix != nil { + appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *rule.Properties.DestinationAddressPrefix) + } + for _, p := range rule.Properties.DestinationAddressPrefixes { + if p != nil { + appendIPOrCIDRLinkIfValid(&sdpItem.LinkedItemQueries, *p) + } + } + } + + return sdpItem, nil +} + +func (n networkSecurityRuleWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.NetworkNetworkSecurityGroup, + azureshared.NetworkApplicationSecurityGroup, + stdlib.NetworkIP, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_rule +func (n networkSecurityRuleWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_network_security_rule.id", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions-reference#microsoftnetwork +func (n networkSecurityRuleWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Network/networkSecurityGroups/securityRules/read", + } +} + +func (n networkSecurityRuleWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/network-security-rule_test.go b/sources/azure/manual/network-security-rule_test.go new file mode 100644 index 00000000..5919b827 --- /dev/null +++ b/sources/azure/manual/network-security-rule_test.go @@ -0,0 +1,300 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "go.uber.org/mock/gomock" + + "github.com/overmindtech/cli/go/discovery" + sdp "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" +) + +type mockSecurityRulesPager struct { + pages []armnetwork.SecurityRulesClientListResponse + index int +} + +func (m *mockSecurityRulesPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockSecurityRulesPager) NextPage(ctx context.Context) (armnetwork.SecurityRulesClientListResponse, error) { + if m.index >= len(m.pages) { + return armnetwork.SecurityRulesClientListResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type errorSecurityRulesPager struct{} + +func (e *errorSecurityRulesPager) More() bool { + return true +} + +func (e *errorSecurityRulesPager) NextPage(ctx context.Context) (armnetwork.SecurityRulesClientListResponse, error) { + return armnetwork.SecurityRulesClientListResponse{}, errors.New("pager error") +} + +type testSecurityRulesClient struct { + *mocks.MockSecurityRulesClient + pager clients.SecurityRulesPager +} + +func (t *testSecurityRulesClient) NewListPager(resourceGroupName, networkSecurityGroupName string, options *armnetwork.SecurityRulesClientListOptions) clients.SecurityRulesPager { + return t.pager +} + +func TestNetworkSecurityRule(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + nsgName := "test-nsg" + ruleName := "test-rule" + + t.Run("Get", func(t *testing.T) { + rule := createAzureSecurityRule(ruleName, nsgName) + + mockClient := mocks.NewMockSecurityRulesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, nsgName, ruleName, nil).Return( + armnetwork.SecurityRulesClientGetResponse{ + SecurityRule: *rule, + }, nil) + + testClient := &testSecurityRulesClient{MockSecurityRulesClient: mockClient} + wrapper := manual.NewNetworkSecurityRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(nsgName, ruleName) + 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.NetworkSecurityRule.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkSecurityRule, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != shared.CompositeLookupKey(nsgName, ruleName) { + t.Errorf("Expected unique attribute value %s, got %s", shared.CompositeLookupKey(nsgName, ruleName), sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.NetworkNetworkSecurityGroup.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: nsgName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_EmptyRuleName", func(t *testing.T) { + mockClient := mocks.NewMockSecurityRulesClient(ctrl) + testClient := &testSecurityRulesClient{MockSecurityRulesClient: mockClient} + + wrapper := manual.NewNetworkSecurityRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(nsgName, "") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when rule name is empty, but got nil") + } + }) + + t.Run("Get_InsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSecurityRulesClient(ctrl) + testClient := &testSecurityRulesClient{MockSecurityRulesClient: mockClient} + + wrapper := manual.NewNetworkSecurityRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], nsgName, true) + if qErr == nil { + t.Error("Expected error when providing insufficient query parts, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + rule1 := createAzureSecurityRule("rule-1", nsgName) + rule2 := createAzureSecurityRule("rule-2", nsgName) + + mockClient := mocks.NewMockSecurityRulesClient(ctrl) + mockPager := &mockSecurityRulesPager{ + pages: []armnetwork.SecurityRulesClientListResponse{ + { + SecurityRuleListResult: armnetwork.SecurityRuleListResult{ + Value: []*armnetwork.SecurityRule{rule1, rule2}, + }, + }, + }, + } + + testClient := &testSecurityRulesClient{ + MockSecurityRulesClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewNetworkSecurityRule(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, wrapper.Scopes()[0], nsgName, 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.Fatalf("Expected no validation error, got: %v", err) + } + if item.GetType() != azureshared.NetworkSecurityRule.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkSecurityRule, item.GetType()) + } + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSecurityRulesClient(ctrl) + testClient := &testSecurityRulesClient{MockSecurityRulesClient: mockClient} + + wrapper := manual.NewNetworkSecurityRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("Search_RuleWithNilName", func(t *testing.T) { + validRule := createAzureSecurityRule("valid-rule", nsgName) + + mockClient := mocks.NewMockSecurityRulesClient(ctrl) + mockPager := &mockSecurityRulesPager{ + pages: []armnetwork.SecurityRulesClientListResponse{ + { + SecurityRuleListResult: armnetwork.SecurityRuleListResult{ + Value: []*armnetwork.SecurityRule{ + {Name: nil, ID: strPtr("/some/id")}, + validRule, + }, + }, + }, + }, + } + + testClient := &testSecurityRulesClient{ + MockSecurityRulesClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewNetworkSecurityRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable := adapter.(discovery.SearchableAdapter) + sdpItems, err := searchable.Search(ctx, wrapper.Scopes()[0], nsgName, 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)) + } + if sdpItems[0].UniqueAttributeValue() != shared.CompositeLookupKey(nsgName, "valid-rule") { + t.Errorf("Expected unique value %s, got %s", shared.CompositeLookupKey(nsgName, "valid-rule"), sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("security rule not found") + + mockClient := mocks.NewMockSecurityRulesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, nsgName, "nonexistent-rule", nil).Return( + armnetwork.SecurityRulesClientGetResponse{}, expectedErr) + + testClient := &testSecurityRulesClient{MockSecurityRulesClient: mockClient} + wrapper := manual.NewNetworkSecurityRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(nsgName, "nonexistent-rule") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent rule, but got nil") + } + }) + + t.Run("ErrorHandling_Search", func(t *testing.T) { + mockClient := mocks.NewMockSecurityRulesClient(ctrl) + testClient := &testSecurityRulesClient{ + MockSecurityRulesClient: mockClient, + pager: &errorSecurityRulesPager{}, + } + + wrapper := manual.NewNetworkSecurityRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable := adapter.(discovery.SearchableAdapter) + _, err := searchable.Search(ctx, wrapper.Scopes()[0], nsgName, true) + if err == nil { + t.Error("Expected error from pager when NextPage returns an error, but got nil") + } + }) +} + +func createAzureSecurityRule(ruleName, nsgName string) *armnetwork.SecurityRule { + idStr := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/" + nsgName + "/securityRules/" + ruleName + typeStr := "Microsoft.Network/networkSecurityGroups/securityRules" + access := armnetwork.SecurityRuleAccessAllow + direction := armnetwork.SecurityRuleDirectionInbound + protocol := armnetwork.SecurityRuleProtocolAsterisk + priority := int32(100) + return &armnetwork.SecurityRule{ + ID: &idStr, + Name: &ruleName, + Type: &typeStr, + Properties: &armnetwork.SecurityRulePropertiesFormat{ + Access: &access, + Direction: &direction, + Protocol: &protocol, + Priority: &priority, + }, + } +} diff --git a/sources/azure/shared/mocks/mock_security_rules_client.go b/sources/azure/shared/mocks/mock_security_rules_client.go new file mode 100644 index 00000000..1078baa3 --- /dev/null +++ b/sources/azure/shared/mocks/mock_security_rules_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: security-rules-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_security_rules_client.go -package=mocks -source=security-rules-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockSecurityRulesClient is a mock of SecurityRulesClient interface. +type MockSecurityRulesClient struct { + ctrl *gomock.Controller + recorder *MockSecurityRulesClientMockRecorder + isgomock struct{} +} + +// MockSecurityRulesClientMockRecorder is the mock recorder for MockSecurityRulesClient. +type MockSecurityRulesClientMockRecorder struct { + mock *MockSecurityRulesClient +} + +// NewMockSecurityRulesClient creates a new mock instance. +func NewMockSecurityRulesClient(ctrl *gomock.Controller) *MockSecurityRulesClient { + mock := &MockSecurityRulesClient{ctrl: ctrl} + mock.recorder = &MockSecurityRulesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSecurityRulesClient) EXPECT() *MockSecurityRulesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockSecurityRulesClient) Get(ctx context.Context, resourceGroupName, networkSecurityGroupName, securityRuleName string, options *armnetwork.SecurityRulesClientGetOptions) (armnetwork.SecurityRulesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, networkSecurityGroupName, securityRuleName, options) + ret0, _ := ret[0].(armnetwork.SecurityRulesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockSecurityRulesClientMockRecorder) Get(ctx, resourceGroupName, networkSecurityGroupName, securityRuleName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSecurityRulesClient)(nil).Get), ctx, resourceGroupName, networkSecurityGroupName, securityRuleName, options) +} + +// NewListPager mocks base method. +func (m *MockSecurityRulesClient) NewListPager(resourceGroupName, networkSecurityGroupName string, options *armnetwork.SecurityRulesClientListOptions) clients.SecurityRulesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListPager", resourceGroupName, networkSecurityGroupName, options) + ret0, _ := ret[0].(clients.SecurityRulesPager) + return ret0 +} + +// NewListPager indicates an expected call of NewListPager. +func (mr *MockSecurityRulesClientMockRecorder) NewListPager(resourceGroupName, networkSecurityGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListPager", reflect.TypeOf((*MockSecurityRulesClient)(nil).NewListPager), resourceGroupName, networkSecurityGroupName, options) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 9378b5ef..a2a2b4e7 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -36,6 +36,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-network-subnet": {"virtualNetworks", "subnets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName}", "azure-network-virtual-network-peering": {"virtualNetworks", "virtualNetworkPeerings"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/virtualNetworkPeerings/{peeringName}", "azure-network-route": {"routeTables", "routes"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/routeTables/{routeTableName}/routes/{routeName}", + "azure-network-security-rule": {"networkSecurityGroups", "securityRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/networkSecurityGroups/{nsgName}/securityRules/{ruleName}", } if keys, ok := pathKeysMap[resourceType]; ok { From 29235c445841b5bb310355e9a4bf41dabb4631a3 Mon Sep 17 00:00:00 2001 From: TP Honey Date: Thu, 26 Feb 2026 16:10:34 +0000 Subject: [PATCH 20/74] Network tag relationship follow-up (#4004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image Completes deferred integration tests and documentation for GCP network tag relationships. This PR addresses the remaining work from ENG-2757 by adding integration tests for route-to-instance links, instance template relationships, and full E2E SEARCH resolution for instances, alongside updating user-facing documentation to reflect the new blast radius analysis capabilities. --- Linear Issue: [ENG-2769](https://linear.app/overmind/issue/ENG-2769/follow-up-network-tag-relationship-tests-and-documentation-eng-2757)

Open in Web Open in Cursor 

--- > [!NOTE] > **Low Risk** > Documentation-only changes across many GCP type pages; main risk is incorrect or inconsistent link/SEARCH/Terraform mapping guidance due to breadth of edits and a few removals/renames. > > **Overview** > Updates many GCP type docs to standardise formatting (notably supported methods/mappings), expand descriptions, and document additional *resource relationships* used for blast-radius analysis (especially around network tags and routing). > > Adds new type pages for `gcp-certificate-manager-certificate`, `gcp-compute-node-template`, and `gcp-compute-regional-instance-group-manager`, and introduces/expands several new “Possible Links” sections (e.g., routes → forwarding rules via `nextHopIlb`, instance templates/instances → firewalls/routes, load-balancing chain links, and KMS key-version linkages). Also removes the `gcp-big-query-model` and `gcp-compute-region-backend-service` doc pages and updates related docs (e.g., BigQuery dataset linking to routines instead). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit af0298606c9eb842318171ff5e36cd5397e6ed82. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Cursor Agent GitOrigin-RevId: 33503b2d01f32509c4f580050b37bd8fabb39585 --- .../gcp-ai-platform-batch-prediction-job.md | 28 ++- .../gcp/Types/gcp-ai-platform-custom-job.md | 23 ++- .../gcp/Types/gcp-ai-platform-endpoint.md | 25 +-- ...latform-model-deployment-monitoring-job.md | 26 ++- .../gcp/Types/gcp-ai-platform-model.md | 22 ++- .../gcp/Types/gcp-ai-platform-pipeline-job.md | 19 +- .../gcp-artifact-registry-docker-image.md | 12 +- ...big-query-data-transfer-transfer-config.md | 22 ++- .../gcp/Types/gcp-big-query-dataset.md | 26 +-- .../sources/gcp/Types/gcp-big-query-model.md | 26 --- .../gcp/Types/gcp-big-query-routine.md | 17 +- .../sources/gcp/Types/gcp-big-query-table.md | 26 ++- .../Types/gcp-big-table-admin-app-profile.md | 16 +- .../gcp/Types/gcp-big-table-admin-backup.md | 21 ++- .../gcp/Types/gcp-big-table-admin-cluster.md | 15 +- .../gcp/Types/gcp-big-table-admin-instance.md | 17 +- .../gcp/Types/gcp-big-table-admin-table.md | 19 +- .../gcp-certificate-manager-certificate.md | 16 ++ .../Types/gcp-cloud-billing-billing-info.md | 20 ++- .../gcp/Types/gcp-cloud-build-build.md | 26 ++- .../gcp/Types/gcp-cloud-functions-function.md | 18 +- .../Types/gcp-cloud-kms-crypto-key-version.md | 24 +-- .../gcp/Types/gcp-cloud-kms-crypto-key.md | 18 +- .../gcp/Types/gcp-cloud-kms-key-ring.md | 13 +- .../gcp-cloud-resource-manager-project.md | 16 +- .../gcp-cloud-resource-manager-tag-value.md | 11 +- .../sources/gcp/Types/gcp-compute-address.md | 37 +++- .../gcp/Types/gcp-compute-autoscaler.md | 17 +- .../gcp/Types/gcp-compute-backend-service.md | 35 ++-- .../sources/gcp/Types/gcp-compute-disk.md | 29 +-- .../Types/gcp-compute-external-vpn-gateway.md | 12 +- .../sources/gcp/Types/gcp-compute-firewall.md | 19 +- .../gcp/Types/gcp-compute-forwarding-rule.md | 38 +++- .../gcp/Types/gcp-compute-global-address.md | 22 ++- .../gcp-compute-global-forwarding-rule.md | 21 ++- .../gcp/Types/gcp-compute-health-check.md | 13 +- .../Types/gcp-compute-http-health-check.md | 13 +- .../sources/gcp/Types/gcp-compute-image.md | 42 ++++- .../gcp-compute-instance-group-manager.md | 23 ++- .../gcp/Types/gcp-compute-instance-group.md | 16 +- .../Types/gcp-compute-instance-template.md | 40 +++-- .../sources/gcp/Types/gcp-compute-instance.md | 52 +++++- .../gcp/Types/gcp-compute-instant-snapshot.md | 14 +- .../gcp/Types/gcp-compute-machine-image.md | 35 ++-- .../gcp-compute-network-endpoint-group.md | 18 +- .../sources/gcp/Types/gcp-compute-network.md | 15 +- .../gcp/Types/gcp-compute-node-group.md | 13 +- .../gcp/Types/gcp-compute-node-template.md | 23 +++ .../sources/gcp/Types/gcp-compute-project.md | 24 ++- .../gcp-compute-public-delegated-prefix.md | 16 +- .../gcp-compute-region-backend-service.md | 31 ---- .../Types/gcp-compute-region-commitment.md | 12 +- ...compute-regional-instance-group-manager.md | 39 ++++ .../gcp/Types/gcp-compute-reservation.md | 12 +- .../sources/gcp/Types/gcp-compute-route.md | 22 ++- .../sources/gcp/Types/gcp-compute-router.md | 17 +- .../gcp/Types/gcp-compute-security-policy.md | 13 +- .../sources/gcp/Types/gcp-compute-snapshot.md | 20 ++- .../gcp/Types/gcp-compute-ssl-certificate.md | 11 +- .../gcp/Types/gcp-compute-ssl-policy.md | 12 +- .../gcp/Types/gcp-compute-subnetwork.md | 16 +- .../Types/gcp-compute-target-http-proxy.md | 18 +- .../Types/gcp-compute-target-https-proxy.md | 18 +- .../gcp/Types/gcp-compute-target-pool.md | 16 +- .../sources/gcp/Types/gcp-compute-url-map.md | 14 +- .../gcp/Types/gcp-compute-vpn-gateway.md | 14 +- .../gcp/Types/gcp-compute-vpn-tunnel.md | 18 +- .../gcp/Types/gcp-container-cluster.md | 34 ++-- .../gcp/Types/gcp-container-node-pool.md | 31 +++- .../gcp/Types/gcp-dataform-repository.md | 22 ++- .../gcp/Types/gcp-dataplex-aspect-type.md | 12 +- .../gcp/Types/gcp-dataplex-data-scan.md | 17 +- .../gcp/Types/gcp-dataplex-entry-group.md | 12 +- .../Types/gcp-dataproc-autoscaling-policy.md | 13 +- .../sources/gcp/Types/gcp-dataproc-cluster.md | 40 +++-- .../sources/gcp/Types/gcp-dns-managed-zone.md | 16 +- .../Types/gcp-essential-contacts-contact.md | 12 +- .../sources/gcp/Types/gcp-file-instance.md | 16 +- .../docs/sources/gcp/Types/gcp-iam-role.md | 9 +- .../gcp/Types/gcp-iam-service-account-key.md | 14 +- .../gcp/Types/gcp-iam-service-account.md | 17 +- .../sources/gcp/Types/gcp-logging-bucket.md | 19 +- .../sources/gcp/Types/gcp-logging-link.md | 20 ++- .../gcp/Types/gcp-logging-saved-query.md | 11 +- .../sources/gcp/Types/gcp-logging-sink.md | 22 ++- .../gcp/Types/gcp-monitoring-alert-policy.md | 13 +- .../Types/gcp-monitoring-custom-dashboard.md | 12 +- .../gcp-monitoring-notification-channel.md | 17 +- .../sources/gcp/Types/gcp-orgpolicy-policy.md | 18 +- .../gcp/Types/gcp-pub-sub-subscription.md | 26 +-- .../sources/gcp/Types/gcp-pub-sub-topic.md | 22 ++- .../sources/gcp/Types/gcp-redis-instance.md | 17 +- .../sources/gcp/Types/gcp-run-revision.md | 30 ++-- .../docs/sources/gcp/Types/gcp-run-service.md | 29 +-- .../gcp/Types/gcp-secret-manager-secret.md | 15 +- ...nter-management-security-center-service.md | 18 +- .../Types/gcp-service-directory-endpoint.md | 12 +- .../gcp/Types/gcp-service-usage-service.md | 18 +- .../sources/gcp/Types/gcp-spanner-database.md | 24 ++- .../sources/gcp/Types/gcp-spanner-instance.md | 14 +- .../gcp/Types/gcp-sql-admin-backup-run.md | 17 +- .../sources/gcp/Types/gcp-sql-admin-backup.md | 23 +-- .../gcp/Types/gcp-sql-admin-instance.md | 26 +-- .../Types/gcp-storage-bucket-iam-policy.md | 36 ++++ .../sources/gcp/Types/gcp-storage-bucket.md | 26 ++- .../gcp-storage-transfer-transfer-job.md | 23 ++- .../gcp-ai-platform-batch-prediction-job.json | 4 +- .../gcp/data/gcp-ai-platform-custom-job.json | 3 +- .../gcp/data/gcp-ai-platform-endpoint.json | 5 +- ...tform-model-deployment-monitoring-job.json | 6 +- .../gcp/data/gcp-ai-platform-model.json | 5 +- .../data/gcp-ai-platform-pipeline-job.json | 2 +- .../gcp-artifact-registry-docker-image.json | 2 +- ...g-query-data-transfer-transfer-config.json | 3 +- .../gcp/data/gcp-big-query-dataset.json | 12 +- .../sources/gcp/data/gcp-big-query-model.json | 16 -- .../gcp/data/gcp-big-query-routine.json | 10 +- .../sources/gcp/data/gcp-big-query-table.json | 22 ++- .../data/gcp-big-table-admin-app-profile.json | 2 +- .../gcp/data/gcp-big-table-admin-backup.json | 5 +- .../gcp/data/gcp-big-table-admin-cluster.json | 2 +- .../data/gcp-big-table-admin-instance.json | 15 +- .../gcp/data/gcp-big-table-admin-table.json | 14 +- .../gcp-certificate-manager-certificate.json | 17 ++ .../data/gcp-cloud-billing-billing-info.json | 6 +- .../gcp/data/gcp-cloud-build-build.json | 4 +- .../data/gcp-cloud-functions-function.json | 2 +- .../gcp-cloud-kms-crypto-key-version.json | 20 +++ .../gcp/data/gcp-cloud-kms-crypto-key.json | 15 +- .../gcp/data/gcp-cloud-kms-key-ring.json | 11 +- .../gcp-cloud-resource-manager-project.json | 2 +- .../gcp-cloud-resource-manager-tag-value.json | 2 +- .../sources/gcp/data/gcp-compute-address.json | 7 +- .../gcp/data/gcp-compute-autoscaler.json | 6 +- .../gcp/data/gcp-compute-backend-service.json | 14 +- .../sources/gcp/data/gcp-compute-disk.json | 6 +- .../gcp-compute-external-vpn-gateway.json | 2 +- .../gcp/data/gcp-compute-firewall.json | 12 +- .../gcp/data/gcp-compute-forwarding-rule.json | 9 +- .../gcp/data/gcp-compute-global-address.json | 8 +- .../gcp-compute-global-forwarding-rule.json | 5 +- .../gcp/data/gcp-compute-health-check.json | 5 +- .../data/gcp-compute-http-health-check.json | 2 +- .../sources/gcp/data/gcp-compute-image.json | 15 +- .../gcp-compute-instance-group-manager.json | 3 +- .../gcp/data/gcp-compute-instance-group.json | 7 +- .../data/gcp-compute-instance-template.json | 8 +- .../gcp/data/gcp-compute-instance.json | 17 +- .../data/gcp-compute-instant-snapshot.json | 6 +- .../gcp/data/gcp-compute-machine-image.json | 8 +- .../gcp-compute-network-endpoint-group.json | 2 +- .../sources/gcp/data/gcp-compute-network.json | 7 +- .../gcp/data/gcp-compute-node-group.json | 5 +- .../gcp/data/gcp-compute-node-template.json | 19 ++ .../sources/gcp/data/gcp-compute-project.json | 35 +++- .../gcp-compute-public-delegated-prefix.json | 2 +- .../gcp-compute-region-backend-service.json | 21 --- .../data/gcp-compute-region-commitment.json | 6 +- ...mpute-regional-instance-group-manager.json | 23 +++ .../gcp/data/gcp-compute-reservation.json | 2 +- .../sources/gcp/data/gcp-compute-route.json | 7 +- .../sources/gcp/data/gcp-compute-router.json | 2 +- .../gcp/data/gcp-compute-security-policy.json | 2 +- .../gcp/data/gcp-compute-snapshot.json | 8 +- .../gcp/data/gcp-compute-ssl-certificate.json | 2 +- .../gcp/data/gcp-compute-ssl-policy.json | 2 +- .../gcp/data/gcp-compute-subnetwork.json | 7 +- .../data/gcp-compute-target-http-proxy.json | 6 +- .../data/gcp-compute-target-https-proxy.json | 2 +- .../gcp/data/gcp-compute-target-pool.json | 2 +- .../sources/gcp/data/gcp-compute-url-map.json | 6 +- .../gcp/data/gcp-compute-vpn-gateway.json | 6 +- .../gcp/data/gcp-compute-vpn-tunnel.json | 2 +- .../gcp/data/gcp-container-cluster.json | 4 +- .../gcp/data/gcp-container-node-pool.json | 5 +- .../gcp/data/gcp-dataform-repository.json | 3 +- .../gcp/data/gcp-dataplex-aspect-type.json | 2 +- .../gcp/data/gcp-dataplex-data-scan.json | 7 +- .../gcp/data/gcp-dataplex-entry-group.json | 2 +- .../data/gcp-dataproc-autoscaling-policy.json | 2 +- .../gcp/data/gcp-dataproc-cluster.json | 5 +- .../gcp/data/gcp-dns-managed-zone.json | 7 +- .../data/gcp-essential-contacts-contact.json | 2 +- .../sources/gcp/data/gcp-file-instance.json | 7 +- .../docs/sources/gcp/data/gcp-iam-role.json | 2 +- .../gcp/data/gcp-iam-service-account-key.json | 7 +- .../gcp/data/gcp-iam-service-account.json | 2 +- .../sources/gcp/data/gcp-logging-bucket.json | 8 +- .../sources/gcp/data/gcp-logging-link.json | 7 +- .../gcp/data/gcp-logging-saved-query.json | 2 +- .../sources/gcp/data/gcp-logging-sink.json | 3 +- .../gcp/data/gcp-monitoring-alert-policy.json | 6 +- .../data/gcp-monitoring-custom-dashboard.json | 2 +- .../gcp-monitoring-notification-channel.json | 5 +- .../gcp/data/gcp-orgpolicy-policy.json | 5 +- .../gcp/data/gcp-pub-sub-subscription.json | 12 +- .../sources/gcp/data/gcp-pub-sub-topic.json | 17 +- .../sources/gcp/data/gcp-redis-instance.json | 2 +- .../sources/gcp/data/gcp-run-revision.json | 3 +- .../sources/gcp/data/gcp-run-service.json | 2 +- .../gcp/data/gcp-secret-manager-secret.json | 7 +- ...er-management-security-center-service.json | 5 +- .../data/gcp-service-directory-endpoint.json | 6 +- .../gcp/data/gcp-service-usage-service.json | 7 +- .../gcp/data/gcp-spanner-database.json | 9 +- .../gcp/data/gcp-spanner-instance.json | 6 +- .../gcp/data/gcp-sql-admin-backup-run.json | 8 +- .../gcp/data/gcp-sql-admin-backup.json | 5 +- .../gcp/data/gcp-sql-admin-instance.json | 3 +- .../data/gcp-storage-bucket-iam-policy.json | 28 +++ .../sources/gcp/data/gcp-storage-bucket.json | 14 +- .../gcp-storage-transfer-transfer-job.json | 3 +- .../gcp/dynamic/adapters/compute-firewall.go | 4 +- .../adapters/compute-instance-template.go | 19 +- sources/gcp/dynamic/adapters/compute-route.go | 2 +- .../integration-tests/network-tags_test.go | 166 +++++++++++++++--- sources/gcp/shared/linker.go | 26 ++- 217 files changed, 2071 insertions(+), 1086 deletions(-) delete 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-certificate-manager-certificate.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-template.md delete 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-regional-instance-group-manager.md create mode 100644 docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket-iam-policy.md delete 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-certificate-manager-certificate.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key-version.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-template.json delete 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-regional-instance-group-manager.json create mode 100644 docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket-iam-policy.json 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 index 58334249..6b823b2a 100644 --- 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 @@ -3,32 +3,42 @@ 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 +A **Batch Prediction Job** in Google Cloud’s AI Platform (now part of Vertex AI) lets you run large-scale, asynchronous inference on a saved Machine Learning model. Instead of serving predictions request-by-request, you supply a dataset stored in Cloud Storage or BigQuery and the service spins up the necessary compute, distributes the workload, writes the predictions to your chosen destination, and then shuts itself down. This is ideal for one-off or periodic scoring of very large datasets. +Official documentation: https://cloud.google.com/vertex-ai/docs/predictions/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' +* `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-endpoint`](/sources/gcp/Types/gcp-ai-platform-endpoint) + +A Batch Prediction Job can read from a Model that is already deployed to an Endpoint; when that is the case the job records the Endpoint name it referenced, creating this link. + ### [`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. +Every Batch Prediction Job must specify the Model it will use for inference. The job stores the fully-qualified model resource name, creating a direct dependency on 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. +The job may take its input instances from a BigQuery table or write its prediction outputs to one. When either the source or destination is a BigQuery table, that table is linked to the job. ### [`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. +If customer-managed encryption keys (CMEK) are chosen, the Batch Prediction Job references the CryptoKey that encrypts the job metadata and any intermediate files, producing this link. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +When the job is configured for private service access, it is attached to a specific VPC network for egress. That VPC network is therefore related to, and linked from, the job. ### [`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. +The Batch Prediction Job executes under a user-specified or default service account, which needs permission to read the model and the input data and to write outputs. That execution identity is linked here. ### [`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. +Cloud Storage buckets are commonly used both for the input artefacts (CSV/JSON/TFRecord files) and for the output prediction files. Any bucket mentioned in the job’s specification is linked to the job. \ No newline at end of file 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 index 4a4c1935..cd1c2e43 100644 --- 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 @@ -3,33 +3,38 @@ 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. +A Vertex AI / AI Platform Custom Job represents an ad-hoc machine-learning workload that you want Google Cloud to run on managed infrastructure. By pointing the job at a custom container image or a Python package, you can execute training, hyper-parameter tuning or batch-processing logic with fine-grained control over machine types, accelerators, networking and encryption. The job definition is submitted to the `projects.locations.customJobs` API and Google Cloud provisions the required compute, streams logs, stores artefacts and tears the resources down once the job finishes. 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`~~ +* `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`. +A successful Custom Job can optionally call `model.upload()` or configure `model_to_upload`, causing Vertex AI to register a `Model` resource containing the trained artefacts. Overmind links the job to the resulting `gcp-ai-platform-model` so you can trace how the model was produced. + +### [`gcp-artifact-registry-docker-image`](/sources/gcp/Types/gcp-artifact-registry-docker-image) + +Custom Jobs usually run inside user-supplied container images. When the image is stored in Artifact Registry, Overmind records a link between the job and the specific `gcp-artifact-registry-docker-image` it pulled, making it easy to audit code and dependency provenance. ### [`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`. +If you enable customer-managed encryption keys (CMEK) for the job, Google Cloud encrypts logs, checkpoints and model files with the specified KMS key. The job therefore references a `gcp-cloud-kms-crypto-key`, which Overmind surfaces to highlight encryption dependencies and key-rotation risks. ### [`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`. +Custom Jobs can be configured to run on a private VPC network (VPC-SC or VPC-hosted training). In that case the job is associated with the chosen `gcp-compute-network`, allowing Overmind to show ingress/egress paths and potential network exposure. ### [`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`. +Vertex AI executes the workload under a user-specified or default service account. The job’s permissions—and hence its ability to read data, write artefacts or call other Google APIs—are determined by this `gcp-iam-service-account`. Overmind links them to flag overly-privileged identities. ### [`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. +Training data, intermediate checkpoints and exported models are commonly read from or written to Cloud Storage. The Custom Job specifies bucket URIs (e.g., `gs://my-dataset/*`, `gs://my-model-output/`). Overmind connects the job to each referenced `gcp-storage-bucket` so you can assess data residency and access controls. \ No newline at end of file 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 index bcbe74fe..52998ccf 100644 --- 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 @@ -3,33 +3,38 @@ 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 +A **Google Cloud AI Platform Endpoint** (now part of Vertex AI) is a regional, fully-managed HTTPS entry point that receives online prediction requests and routes them to one or more deployed models. Endpoints let you perform low-latency, autoscaled inference, apply access controls, add request/response logging and attach monitoring jobs. +Official documentation: https://cloud.google.com/vertex-ai/docs/predictions/getting-predictions#deploy_model_to_endpoint + ## Supported Methods -- `GET`: Get a gcp-ai-platform-endpoint by its "name" -- `LIST`: List all gcp-ai-platform-endpoint -- ~~`SEARCH`~~ +* `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. +An Endpoint hosts one or more *DeployedModels*, each of which references a standalone AI Platform/Vertex AI Model resource. The link shows which models are currently deployed to, or have traffic routed through, the endpoint. ### [`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. +If data-drift or prediction-quality monitoring has been enabled, a Model Deployment Monitoring Job is attached to the endpoint. This relationship identifies the monitoring configuration that observes traffic on the endpoint. ### [`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. +Prediction request and response payloads can be logged to a BigQuery table when logging is enabled on the endpoint. The link indicates which table is used as the logging sink for the endpoint’s traffic. ### [`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. +Customer-managed encryption keys (CMEK) from Cloud KMS can be specified to encrypt endpoint resources at rest. This link reveals the KMS key protecting the endpoint and its deployed models. ### [`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. +Endpoints can be configured for private service access, allowing prediction traffic to stay within a specified VPC network. The relationship points to the Compute Network that provides the private connectivity for the endpoint. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Each deployed model on an endpoint runs under a service account whose permissions govern access to other GCP resources (e.g., storage buckets, KMS keys). The link shows which IAM service account is associated with the endpoint’s runtime. \ No newline at end of file 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 index 69cb6b26..1995c763 100644 --- 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 @@ -3,30 +3,38 @@ 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. +Google Cloud’s Model Deployment Monitoring Job is a managed Vertex AI (formerly AI Platform) service that continuously analyses a deployed model’s predictions to detect data drift, prediction drift and skew between training and online data. A job is attached to one or more deployed models on an Endpoint and periodically samples incoming predictions, calculates statistics, raises alerts and writes monitoring reports to BigQuery or Cloud Storage. +Official documentation: https://cloud.google.com/vertex-ai/docs/model-monitoring/overview -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' +* `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. +The monitoring job is created against a specific Endpoint; it inspects the request/response traffic that the Endpoint receives for the deployed model versions. ### [`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. +Each job’s `modelDeploymentMonitoringObjectiveConfigs` identifies the Model (or model version) whose predictions are being monitored for drift or skew. + +### [`gcp-big-query-table`](/sources/gcp/Types/gcp-big-query-table) + +If BigQuery is chosen as the analysis destination, the job writes sampled prediction data and computed statistics into a BigQuery table referenced by this link. ### [`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. +The `encryptionSpec.kmsKeyName` field can point to a customer-managed KMS key that encrypts all monitoring artefacts produced by the job. ### [`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. +Alerting rules created by the job use Cloud Monitoring notification channels (e-mail, Pub/Sub, SMS, etc.) to notify operators when drift thresholds are breached. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +When Cloud Storage is selected, the job stores prediction samples, intermediate files and final monitoring reports in a user-provided bucket. \ No newline at end of file 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 index 6e685c19..994733f6 100644 --- 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 @@ -3,29 +3,33 @@ 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 +A GCP AI Platform Model (now part of Vertex AI) is a logical container that holds the metadata and artefacts required to serve machine-learning predictions. A model record points to one or more model versions or container images, the Cloud Storage location of the trained parameters, and optional encryption settings. Models are deployed to Endpoints for online prediction or used directly in batch/streaming inference jobs. 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`~~ +* `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. +A model is deployed to one or more Endpoints. The link shows where this model is currently serving traffic or could be routed for prediction. ### [`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. +Training or transformation Pipeline Jobs often create or update Model resources; linking them highlights which automated workflow produced the model and therefore which code/data lineage applies. ### [`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. +If the model is served via a custom prediction container, the Model record references a Docker image stored in Artifact Registry. This link surfaces that underlying image and its associated vulnerabilities. ### [`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. +Models can be protected with customer-managed encryption keys (CMEK). Overmind links the model to the specific KMS key to expose encryption scope and key rotation risks. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +The model’s artefacts (e.g., SavedModel, scikit-learn pickle, PyTorch state) reside in a Cloud Storage bucket referenced by `artifactUri`. Linking to the bucket reveals data-at-rest location and its IAM policy. \ No newline at end of file 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 index d15fdf9f..cca2e5e6 100644 --- 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 @@ -3,29 +3,30 @@ 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). +A GCP AI Platform Pipeline Job (now part of Vertex AI Pipelines) represents a single execution of a machine-learning workflow defined in a Kubeflow/Vertex AI pipeline. The job orchestrates a directed acyclic graph (DAG) of pipeline components such as data preparation, model training and evaluation, and optionally deployment. Each run is stored as a resource that tracks the DAG definition, runtime parameters, execution state, logs and metadata. +Official documentation: https://cloud.google.com/vertex-ai/docs/pipelines/introduction + ## Supported Methods -- `GET`: Get a gcp-ai-platform-pipeline-job by its "name" -- `LIST`: List all gcp-ai-platform-pipeline-job -- ~~`SEARCH`~~ +* `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. +If the pipeline job is configured to use customer-managed encryption keys (CMEK), the key referenced here encrypts pipeline artefacts such as metadata, intermediate files and model checkpoints. ### [`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. +Pipeline components that run in custom training containers or Dataflow/Dataproc jobs may be attached to a specific VPC network to control egress, ingress and private service access. The pipeline job therefore has an implicit or explicit relationship with the VPC network used at execution time. ### [`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. +The pipeline job executes under a service account which grants it permissions to create and manage downstream resources (e.g. training jobs, storage objects, BigQuery datasets). Overmind links the job to the service account that appears in its runtime configuration. ### [`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. +Vertex AI Pipelines store pipeline definitions, intermediate artefacts, and output models in Cloud Storage. A pipeline job will reference one or more buckets for source code, artefacts and logging, so Overmind creates links to each bucket it touches. \ No newline at end of file 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 index ce46b462..ac75f64a 100644 --- 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 @@ -3,15 +3,15 @@ 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 +A GCP Artifact Registry Docker Image represents a single container image stored within Google Cloud Artifact Registry. Artifact Registry is Google Cloud’s fully-managed, secure, and scalable repository service that allows teams to store, manage and secure their build artefacts, including Docker container images. Each Docker image is identified by its path in the form `projects/{project}/locations/{location}/repositories/{repository}/dockerImages/{image}` and can hold multiple tags and versions. Managing images through Artifact Registry enables fine-grained IAM permissions, vulnerability scanning, and seamless integration with Cloud Build and Cloud Run. +For more information, see the official documentation: https://cloud.google.com/artifact-registry/docs/docker **Terrafrom Mappings:** -- `google_artifact_registry_docker_image.name` + * `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. +* `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. \ No newline at end of file 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 index 14a1cebc..9c6bc4e5 100644 --- 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 @@ -3,29 +3,33 @@ 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 +A BigQuery Data Transfer transfer configuration defines the schedule, destination dataset and credentials that the BigQuery Data Transfer Service will use to load data from a supported SaaS application, Google service or external data source into BigQuery. Each configuration specifies when transfers should run, the parameters required by the source system and, optionally, Pub/Sub notification settings and Cloud KMS encryption keys. +For a full description of the resource see the Google Cloud documentation: https://cloud.google.com/bigquery/docs/reference/datatransfer/rest/v1/projects.locations.transferConfigs **Terrafrom Mappings:** -- `google_bigquery_data_transfer_config.id` + * `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. +* `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. +The transfer configuration writes its imported data into a specific BigQuery dataset; the dataset’s identifier is stored in the configuration’s `destinationDatasetId` field. Overmind therefore links the config to the dataset that will receive the transferred data. ### [`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. +If the destination dataset is protected with customer-managed encryption keys (CMEK), the transfer runs inherit that key. Consequently, the configuration is indirectly associated with the Cloud KMS crypto key that encrypts the loaded tables, allowing Overmind to surface encryption-related risks. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Transfers execute using a dedicated service account (`project-number@gcp-sa-bigquerydt.iam.gserviceaccount.com`) or, in some cases, a user-provided service account. The configuration stores this principal, and appropriate IAM roles must be granted. Overmind links the transfer config to the service account to assess permission scopes. ### [`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. +A transfer configuration can be set to publish run status notifications to a Pub/Sub topic specified in its `notificationPubsubTopic` field. Overmind links the configuration to that topic so that message-flow and permissions between the two resources can be evaluated. \ No newline at end of file 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 index cccc66c4..820d662b 100644 --- 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 @@ -3,37 +3,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 +A Google Cloud BigQuery Dataset is a logical container that holds tables, views, routines (stored procedures and functions) and metadata, and defines the geographic location where the underlying data is stored. Datasets also act as the administrative boundary for access-control policies and encryption configuration. For a full description, see the official documentation: https://cloud.google.com/bigquery/docs/datasets-intro **Terrafrom Mappings:** -- `google_bigquery_dataset.dataset_id` + * `google_bigquery_dataset.dataset_id` + * `google_bigquery_dataset_iam_binding.dataset_id` + * `google_bigquery_dataset_iam_member.dataset_id` + * `google_bigquery_dataset_iam_policy.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`~~ +* `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. +Datasets can reference, copy from or authorise access to other BigQuery datasets, so Overmind may surface links where cross-dataset operations or shared access exist. -### [`gcp-big-query-model`](/sources/gcp/Types/gcp-big-query-model) +### [`gcp-big-query-routine`](/sources/gcp/Types/gcp-big-query-routine) -Every BigQuery ML model belongs to exactly one dataset. All models whose `dataset_id` matches this dataset will be linked. +Every BigQuery routine (stored procedure or user-defined function) resides inside a specific dataset; therefore routines are children of the current dataset. ### [`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. +Tables and views are stored within a dataset. All tables that belong to this dataset will be linked here. ### [`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. +If customer-managed encryption is enabled, the dataset (and everything inside it) may be encrypted with a specific Cloud KMS crypto key. This link shows which key is in use. ### [`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. +Access to a dataset is granted via IAM, often to service accounts. Linked service accounts represent principals that have explicit permissions on the dataset. \ No newline at end of file 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 deleted file mode 100644 index d570b152..00000000 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-big-query-model.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -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 index b3534ec1..7ccf9fa9 100644 --- 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 @@ -3,20 +3,25 @@ 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). +A BigQuery Routine is a reusable piece of SQL or JavaScript logic—such as a stored procedure, user-defined function (UDF), or table-valued function—stored inside a BigQuery dataset. Routines let you encapsulate complex transformations, calculations, or business rules and call them from queries just like native BigQuery functions. They can reference other BigQuery objects (tables, views, models, etc.) and may be version-controlled and secured independently of the data they operate on. +Official documentation: https://cloud.google.com/bigquery/docs/reference/rest/v2/routines **Terrafrom Mappings:** -- `google_bigquery_routine.routine_id` + * `google_bigquery_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" +* `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. +A routine is always contained within exactly one BigQuery dataset. The link lets you trace from a routine to its parent dataset to understand data location, access controls, and retention policies that also apply to the routine. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +If a routine’s SQL references an external table backed by Cloud Storage, or if the routine loads/stages data via the `LOAD DATA` or `EXPORT DATA` statements, the routine implicitly depends on the corresponding Cloud Storage bucket. This link surfaces that dependency so you can assess the impact of bucket-level permissions and lifecycle rules on the routine’s execution. \ No newline at end of file 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 index 77f8bde5..7620f523 100644 --- 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 @@ -3,24 +3,36 @@ 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 +A BigQuery table is the fundamental storage unit inside Google Cloud BigQuery. It holds the actual rows of structured data that can be queried with SQL, shared, exported or used to build materialised views and machine-learning models. Tables live inside a dataset, can be partitioned or clustered, and may be encrypted either with Google-managed keys or customer-managed keys stored in Cloud KMS. They can also act as logical wrappers around external data held in Cloud Storage. +Official documentation: https://cloud.google.com/bigquery/docs/tables **Terrafrom Mappings:** -- `google_bigquery_table.id` + * `google_bigquery_table.id` + * `google_bigquery_table_iam_binding.dataset_id` + * `google_bigquery_table_iam_member.dataset_id` + * `google_bigquery_table_iam_policy.dataset_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" +* `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. +The dataset is the immediate parent container of the table; every table must belong to exactly one dataset and inherits default encryption, location and IAM settings from it. + +### [`gcp-big-query-table`](/sources/gcp/Types/gcp-big-query-table) + +BigQuery tables can reference, copy from, or be copied to other tables (for example when creating snapshots, clones, views with explicit table references or COPY jobs). Such relationships are captured as links between table resources. ### [`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. +If the table (or its parent dataset) is configured to use customer-managed encryption, it points to the Cloud KMS CryptoKey that protects the data at rest. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +An external BigQuery table may use objects stored in a Cloud Storage bucket as its underlying data source; in that case the table is linked to the bucket holding those objects. \ No newline at end of file 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 index 76c988b0..ee8556c7 100644 --- 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 @@ -3,25 +3,25 @@ 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 +A Bigtable **App Profile** is a logical configuration that tells Google Cloud Bigtable how client traffic for a particular application should be routed to one or more clusters within an instance. It lets you choose between single-cluster routing (for the lowest latency within a specific region) or multi-cluster routing (for higher availability across several regions) and also defines the consistency model that the application will see. Because app profiles govern the path that live data takes, mis-configuration can lead to increased latency, unexpected fail-over behaviour, or cross-region egress costs. +Official documentation: https://cloud.google.com/bigtable/docs/app-profiles **Terrafrom Mappings:** -- `google_bigtable_app_profile.id` + * `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. +* `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. +An App Profile points client traffic towards one or more specific clusters. Each routing policy within the profile references the cluster identifiers defined by `gcp-big-table-admin-cluster`. Observing this link lets you see which clusters will receive traffic from the application and assess redundancy or regional placement risks. ### [`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. +Every App Profile exists inside a single Bigtable instance. Linking to `gcp-big-table-admin-instance` shows the broader configuration—such as replication settings and all clusters—that frames the context in which the App Profile operates. \ No newline at end of file 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 index 58520f6c..ff9a0e3c 100644 --- 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 @@ -3,25 +3,30 @@ 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 +A Cloud Bigtable Admin Backup represents a point-in-time copy of a single Bigtable table that is stored within the same Bigtable cluster for a user-defined retention period. Back-ups allow you to restore data that has been deleted or corrupted without replaying your entire write history, and they can also be copied to other regions for disaster-recovery purposes. The resource is created, managed and deleted through the Cloud Bigtable Admin API. +Official documentation: https://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" +* `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. +If the current backup is used as the source for a cross-cluster copy, or if multiple back-ups are chained through copy operations, Overmind links the related `gcp-big-table-admin-backup` resources together so you can trace provenance and inheritance of data. ### [`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. +Every backup is physically stored in the Bigtable cluster where it was created. The backup therefore links to its parent `gcp-big-table-admin-cluster`, enabling you to understand locality, storage costs and the failure domain that may affect both the cluster and its back-ups. ### [`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. +A backup is a snapshot of a specific Bigtable table at the moment the backup was taken. This link points back to that source `gcp-big-table-admin-table`, allowing you to see which dataset the backup protects and to assess the impact of schema or data changes. + +### [`gcp-cloud-kms-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) + +When customer-managed encryption (CMEK) is enabled, the backup’s data is encrypted with a particular Cloud KMS key version. Linking to `gcp-cloud-kms-crypto-key-version` lets you audit encryption lineage and verify that the correct key material is being used for protecting the backup. \ No newline at end of file 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 index c0a71a23..8c67c466 100644 --- 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 @@ -3,21 +3,22 @@ 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 +A GCP Bigtable Admin Cluster resource represents the configuration of a single cluster that belongs to a Cloud Bigtable instance. The cluster defines the geographic location where data is stored, the number and type of serving nodes, the storage type (HDD or SSD), autoscaling settings, and any customer-managed encryption keys (CMEK) that protect the data. It is managed through the Cloud Bigtable Admin API, which allows you to create, update, or delete clusters programmatically. +For further details, see Google’s official documentation: https://cloud.google.com/bigtable/docs/instances-clusters-nodes + ## 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" +* `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. +A cluster is always a child of a Bigtable instance. This link represents the parent–child relationship: the instance contains one or more clusters, and every cluster must reference its parent instance. ### [`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. +If Customer-Managed Encryption Keys (CMEK) are enabled, the cluster’s encryption configuration points to the Cloud KMS CryptoKey that is used to encrypt data at rest. This link captures that dependency between the cluster and the key. \ No newline at end of file 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 index 7c8d7a88..f6adbe85 100644 --- 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 @@ -3,22 +3,23 @@ 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 +Cloud Bigtable instances are the top-level administrative containers for all tables and data stored in Bigtable. An instance defines the service tier (production or development), the geographic placement of data through its clusters, and provides the entry point for IAM policy management, encryption settings, labelling and more. For a detailed overview of instances, see the official Google Cloud documentation: https://cloud.google.com/bigtable/docs/instances-clusters-nodes **Terrafrom Mappings:** -- `google_bigtable_instance.name` + * `google_bigtable_instance.name` + * `google_bigtable_instance_iam_binding.instance` + * `google_bigtable_instance_iam_member.instance` + * `google_bigtable_instance_iam_policy.instance` ## Supported Methods -- `GET`: Get a gcp-big-table-admin-instance by its "name" -- `LIST`: List all gcp-big-table-admin-instance -- ~~`SEARCH`~~ +* `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. +Every Bigtable instance is composed of one or more clusters. A `gcp-big-table-admin-cluster` represents the individual cluster resources that reside within, and are owned by, a given Bigtable instance. \ No newline at end of file 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 index a2196b32..f1023ee1 100644 --- 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 @@ -3,28 +3,31 @@ 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. +Google Cloud Bigtable is a scalable NoSQL database service for large analytical and operational workloads. A Bigtable **table** is the primary data container within an instance, organised into rows and column families. The Bigtable Admin API allows you to create, configure, list, and delete tables, as well as manage their IAM policies and column–family schemas. Full details can be found in the official documentation: https://cloud.google.com/bigtable/docs/reference/admin/rest **Terrafrom Mappings:** -- `google_bigtable_table.id` + * `google_bigtable_table.id` + * `google_bigtable_table_iam_binding.instance_name` + * `google_bigtable_table_iam_member.instance_name` + * `google_bigtable_table_iam_policy.instance_name` ## 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. +* `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. +A Bigtable table can have one or more backups. Overmind links a table to its related `gcp-big-table-admin-backup` resources, making it easy to assess how backup configurations might be impacted by changes to the table. ### [`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. +Every table is created inside a single Bigtable instance. This link shows the parent `gcp-big-table-admin-instance` that owns the table so you can understand instance-level settings (such as clusters and IAM) that may affect the table. ### [`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. +Tables may reference each other indirectly through IAM policies or schema design. Overmind links tables to other tables when such relationships are detected, allowing you to trace dependencies across multiple Bigtable tables within or across instances. \ No newline at end of file diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-certificate-manager-certificate.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-certificate-manager-certificate.md new file mode 100644 index 00000000..62956bfb --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-certificate-manager-certificate.md @@ -0,0 +1,16 @@ +--- +title: GCP Certificate Manager Certificate +sidebar_label: gcp-certificate-manager-certificate +--- + +A **GCP Certificate Manager Certificate** represents an SSL/TLS certificate that is stored and managed by Google Cloud Certificate Manager. Certificates configured here can be Google-managed (automatically provisioned and renewed by Google) or self-managed (imported by the user) and can be attached to load balancers, Cloud CDN, or other Google Cloud resources to provide encrypted connections. Managing certificates through Certificate Manager centralises lifecycle operations such as issuance, rotation and revocation, reducing operational overhead and the risk of serving expired certificates. For full details, see the official documentation: https://cloud.google.com/certificate-manager/docs + +**Terrafrom Mappings:** + + * `google_certificate_manager_certificate.id` + +## Supported Methods + +* `GET`: Get GCP Certificate Manager Certificate by "gcp-certificate-manager-certificate-location|gcp-certificate-manager-certificate-name" +* ~~`LIST`~~ +* `SEARCH`: Search for GCP Certificate Manager Certificate by "gcp-certificate-manager-certificate-location" \ No newline at end of file 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 index 59bf138c..a8e0f6ea 100644 --- 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 @@ -3,18 +3,24 @@ 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 +The **Cloud Billing – Billing Info** resource represents the billing configuration that is attached to an individual Google Cloud project. +For a given project it records which Cloud Billing Account is linked, whether billing is currently enabled, and other metadata that controls how usage costs are charged. +The resource is surfaced by the Cloud Billing API endpoint +`cloudbilling.googleapis.com/v1/projects/{projectId}/billingInfo`. +Full details are available in the official Google documentation: +https://cloud.google.com/billing/docs/reference/rest/v1/projects/getBillingInfo + +Knowing the contents of this object allows Overmind to determine, for example, whether a project is running with an unexpectedly disabled billing account or whether it is tied to the correct cost centre before a deployment is made. ## Supported Methods -- `GET`: Get a gcp-cloud-billing-billing-info by its "name" -- ~~`LIST`~~ -- ~~`SEARCH`~~ +* `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. +Every Billing Info object belongs to exactly one Cloud Resource Manager Project. +Overmind creates a link from `gcp-cloud-billing-billing-info` → `gcp-cloud-resource-manager-project` so that users can trace the billing configuration back to the workload and other resources that live inside the same project. \ No newline at end of file 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 index 0ecba466..8dd0b7a4 100644 --- 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 @@ -3,29 +3,37 @@ 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 +A **Cloud Build Build** represents a single execution of Google Cloud Build, Google Cloud’s CI/CD service. Each build contains one or more build steps (Docker containers) that run in sequence or in parallel to compile code, run tests, or package and deploy artefacts. Metadata recorded on the build includes its source, substitutions, images, logs, secrets used, time-stamps, and overall status. +See the official documentation for full details: 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`~~ +* `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. +A build often produces container images and pushes them to Artifact Registry. Overmind links the build to every `gcp-artifact-registry-docker-image` whose digest or tag is declared in the build’s `images` field. + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +Builds can be configured to decrypt secrets with Cloud KMS. If the build specification references a KMS key (for example in `secretEnv`), Overmind records a link to the corresponding `gcp-cloud-kms-crypto-key`. ### [`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. +Cloud Build runs under a service account (`serviceAccount` field). The build is therefore linked to the `gcp-iam-service-account` that actually executes the build steps and accesses other resources. ### [`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`. +Build logs are written to Cloud Logging and can be routed into a custom logging bucket. If log sink routing points the build’s logs to a specific `gcp-logging-bucket`, Overmind associates the two objects. + +### [`gcp-secret-manager-secret`](/sources/gcp/Types/gcp-secret-manager-secret) + +Secrets injected into build steps via `secretEnv` or `availableSecrets` are stored in Secret Manager. A link is created between the build and every `gcp-secret-manager-secret` it consumes. ### [`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. +Cloud Build can pull its source from a Cloud Storage bucket and write build logs or artefacts back to buckets (e.g. via the `logsBucket` or `artifacts` fields). These buckets appear as related `gcp-storage-bucket` resources. \ No newline at end of file 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 index 119b1da2..9a21d973 100644 --- 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 @@ -3,32 +3,32 @@ 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. +Google Cloud Functions is a server-less execution environment that lets you run event-driven code without provisioning or managing servers. A “Function” is the deployed piece of code together with its configuration (runtime, memory/CPU limits, environment variables, ingress/egress settings, triggers and IAM bindings). Documentation: https://cloud.google.com/functions/docs ## Supported Methods -- `GET`: Get a gcp-cloud-functions-function by its "locations|functions" -- ~~`LIST`~~ -- `SEARCH`: Search for gcp-cloud-functions-function by its "locations" +* `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. +A function can reference a Cloud KMS crypto key to decrypt secrets or to use Customer-Managed Encryption Keys (CMEK) for its source code stored in Cloud Storage. Overmind therefore links the function to any KMS keys it is authorised to use. ### [`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. +Each Cloud Function executes as a service account, and other service accounts may be granted permission to invoke or manage it. Overmind links the function to the runtime service account and to any caller or admin accounts discovered in its IAM policy. ### [`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. +Pub/Sub topics are commonly used as event triggers. When a function is configured to fire on messages published to a topic, Overmind records a link between the function and that topic. ### [`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. +Second-generation Cloud Functions are built and deployed as Cloud Run services under the hood. Overmind links the function to the underlying Cloud Run service so you can trace configuration and runtime dependencies. ### [`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. +Cloud Storage buckets can be both event sources (object create/delete triggers) and repositories for a function’s source code during deployment. Overmind links the function to any bucket that serves as a trigger or holds its source archive. \ No newline at end of file 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 index 64bb5960..1a1470ed 100644 --- 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 @@ -1,30 +1,22 @@ --- -title: GCP Cloud KMS Crypto Key Version +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). +A **Cloud KMS CryptoKeyVersion** is an immutable representation of a single piece of key material managed by Google Cloud Key Management Service. Each CryptoKey can have many versions, allowing you to rotate key material without changing the logical key that your workloads use. A version holds state (e.g., `ENABLED`, `DISABLED`, `DESTROYED`), an algorithm specification (RSA, AES-GCM, etc.), and lifecycle metadata such as creation and destruction timestamps. See the official Google documentation for full details: https://cloud.google.com/kms/docs/reference/rest/v1/projects.locations.keyRings.cryptoKeys.cryptoKeyVersions -**Terraform Mappings:** +**Terrafrom Mappings:** -- `google_kms_crypto_key_version.id` + * `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) +* `GET`: Get GCP Cloud Kms Crypto Key Version by "gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name|gcp-cloud-kms-crypto-key-name|gcp-cloud-kms-crypto-key-version-version" +* ~~`LIST`~~ +* `SEARCH`: Search for GCP Cloud Kms Crypto Key Version by "gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name|gcp-cloud-kms-crypto-key-name" ## 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. +A CryptoKeyVersion is always a child of a CryptoKey. The `gcp-cloud-kms-crypto-key` resource represents the logical key, while the current item represents a particular version of that key’s material. \ No newline at end of file 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 index 2813c0fb..7d976d33 100644 --- 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 @@ -3,17 +3,25 @@ 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. +A **Cloud KMS CryptoKey** is the logical resource in Google Cloud that represents a single cryptographic key and its primary metadata. It defines the algorithm, purpose (encryption/decryption, signing/verification, MAC, etc.), rotation schedule, and IAM policy for the key. Each CryptoKey lives inside a Key Ring, can have multiple immutable versions, and is used by Google-managed services (or your own applications) to perform cryptographic operations. Official documentation: https://cloud.google.com/kms/docs/object-hierarchy#key +**Terrafrom Mappings:** + + * `google_kms_crypto_key.id` + ## 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" +* `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-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) + +A CryptoKey is the parent of one or more CryptoKeyVersions. Each version contains the actual key material and its own state (enabled, disabled, destroyed, etc.). Overmind links to these versions so you can inspect individual key material lifecycles and detect risks such as disabled or scheduled-for-destruction versions. + ### [`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. +Every CryptoKey resides within a Key Ring, which provides a namespace and location boundary. This link shows the Key Ring that owns the CryptoKey, allowing you to trace location-specific compliance requirements or IAM inheritance issues. \ No newline at end of file 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 index 5dd85555..e3ce826d 100644 --- 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 @@ -3,20 +3,21 @@ 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). +A **Cloud KMS Key Ring** is a top-level container within Google Cloud KMS that groups one or more CryptoKeys in a specific GCP location (region). It acts as both an organisational unit and an IAM boundary: all CryptoKeys inside a Key Ring inherit the same location and share the same access-control policies. Creating a Key Ring is an irreversible, free operation and is a prerequisite for creating any CryptoKeys. +For full details, see the official documentation: https://cloud.google.com/kms/docs/object-hierarchy#key_rings **Terrafrom Mappings:** -- `google_kms_key_ring.name` + * `google_kms_key_ring.id` ## 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" +* `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 Ring items +* `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. +Each CryptoKey belongs to exactly one Key Ring. Linking a Key Ring to its child `gcp-cloud-kms-crypto-key` items lets Overmind surface all encryption keys that share the same location and IAM policy, making it easier to assess the blast radius of any permission or configuration changes applied to the Key Ring. \ No newline at end of file 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 index 19130df9..011d0d4c 100644 --- 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 @@ -3,18 +3,12 @@ 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: +A Google Cloud Resource Manager Project represents the fundamental organisational unit within Google Cloud Platform (GCP). Every compute, storage or networking asset you create must live inside a Project, which in turn sits under a Folder or Organisation node. Projects provide isolated boundaries for Identity and Access Management (IAM), quotas, billing, API enablement and lifecycle operations such as creation, update, suspension and deletion. By modelling Projects, Overmind can surface risks linked to mis-scoped IAM roles, neglected billing settings or interactions with other resources *before* any change is pushed to production. +Official documentation: https://cloud.google.com/resource-manager/docs/creating-managing-projects -- 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`~~ +* `GET`: Get a gcp-cloud-resource-manager-project by its "name" +* ~~`LIST`~~ +* ~~`SEARCH`~~ \ No newline at end of file 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 index bcf456a7..1ee8ad8a 100644 --- 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 @@ -3,14 +3,15 @@ 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 +A GCP Cloud Resource Manager **Tag Value** is the second layer in Google Cloud’s new tagging hierarchy, sitting beneath a Tag Key and above the individual resources to which it is applied. Together, Tag Keys and Tag Values allow administrators to attach fine-grained, organisation-wide metadata to projects, folders and individual cloud resources, enabling consistent policy enforcement, cost allocation, automation and reporting across an estate. Each Tag Value represents a specific, permitted value for a given Tag Key (e.g. Tag Key `environment` may have Tag Values `production`, `staging`, `test`). +For a full description of Tag Values and how they fit into the tagging system, refer to Google’s documentation: https://cloud.google.com/resource-manager/reference/rest/v3/tagValues. **Terrafrom Mappings:** -- `google_tags_tag_value.name` + * `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. +* `GET`: Get a gcp-cloud-resource-manager-tag-value by its "name" +* ~~`LIST`~~ +* `SEARCH`: Search for TagValues by TagKey. \ No newline at end of file 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 index b9b328cd..1d13138f 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-address.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-address.md @@ -3,29 +3,48 @@ 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 +A GCP Compute Address is a reserved, static IP address that can be either regional (tied to a specific region and VPC network) or global (usable by global load-balancing resources). Once reserved, the address can be attached to forwarding rules, virtual machine (VM) instances, Cloud NAT configurations and other networking resources, ensuring its IP does not change even if the underlying resource is recreated. See the official documentation for full details: https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address. **Terrafrom Mappings:** -- `google_compute_address.name` + * `google_compute_address.name` ## Supported Methods -- `GET`: Get GCP Compute Address by "gcp-compute-address-name" -- `LIST`: List all GCP Compute Address items -- ~~`SEARCH`~~ +* `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). +Static addresses rarely reference one another directly, but Overmind may surface links where an address is used as a reference target (for example, when one resource releases and another takes ownership of the same address). + +### [`gcp-compute-forwarding-rule`](/sources/gcp/Types/gcp-compute-forwarding-rule) + +Regional forwarding rules for Network Load Balancers or protocol forwarding can be configured with a specific static IP. The forwarding rule’s `IPAddress` field points to the Compute Address. + +### [`gcp-compute-global-forwarding-rule`](/sources/gcp/Types/gcp-compute-global-forwarding-rule) + +Global forwarding rules, used by HTTP(S), SSL, or TCP Proxy load balancers, reference a global static IP address. The global forwarding rule therefore links back to the associated Compute Address. + +### [`gcp-compute-instance`](/sources/gcp/Types/gcp-compute-instance) + +A VM instance’s network interface may be assigned a reserved external or internal IP. If an instance uses a static IP, the instance resource contains a link to the corresponding Compute Address. ### [`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. +Internal (private) static addresses are always allocated within a specific VPC network. The Compute Address resource stores the ID of the network from which the IP is taken, creating a link to the Network. + +### [`gcp-compute-public-delegated-prefix`](/sources/gcp/Types/gcp-compute-public-delegated-prefix) + +When you own a public delegated prefix, you can allocate individual static addresses from that range. Each resulting Compute Address records the delegated prefix it belongs to. + +### [`gcp-compute-router`](/sources/gcp/Types/gcp-compute-router) + +Cloud NAT configurations on a Cloud Router can consume one or more reserved external IP addresses. The router’s NAT config lists the Compute Addresses being used, forming a link. ### [`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`. +For regional internal addresses you must specify the subnetwork (IP range) to allocate from. The Compute Address therefore references, and is linked to, the Subnetwork resource. \ No newline at end of file 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 index 7d468e6d..15a418de 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-autoscaler.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-autoscaler.md @@ -3,15 +3,20 @@ 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 +A GCP Compute Autoscaler is a zonal or regional resource that automatically adds or removes VM instances from a managed instance group to keep your application running at the desired performance level and cost. Scaling decisions can be driven by policies based on average CPU utilisation, HTTP load-balancing capacity, Cloud Monitoring metrics, schedules, or per-instance utilisation. Full details can be found in the official documentation: https://cloud.google.com/compute/docs/autoscaler **Terrafrom Mappings:** -- `google_compute_autoscaler.name` + * `google_compute_autoscaler.name` ## Supported Methods -- `GET`: Get GCP Compute Autoscaler by "gcp-compute-autoscaler-name" -- `LIST`: List all GCP Compute Autoscaler items -- ~~`SEARCH`~~ +* `GET`: Get GCP Compute Autoscaler by "gcp-compute-autoscaler-name" +* `LIST`: List all GCP Compute Autoscaler items +* ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-instance-group-manager`](/sources/gcp/Types/gcp-compute-instance-group-manager) + +Every autoscaler is attached to exactly one managed instance group; in the GCP API this relationship is expressed through the `target` field, which points to the relevant `instanceGroupManager` resource. Following this link in Overmind reveals which VM instances the autoscaler is responsible for scaling. \ No newline at end of file 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 index f295c81b..da87ac33 100644 --- 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 @@ -3,27 +3,42 @@ 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 +A Compute Backend Service defines how Google Cloud Load Balancers distribute traffic to one or more back-end targets (Instance Groups, Network Endpoint Groups, or serverless workloads). It specifies the load-balancing algorithm, session affinity, capacity controls, health checks, time-outs, protocol and (optionally) a Cloud Armor security policy. Backend services exist as either regional or global resources, depending on the load balancer type. +For full details see the official Google Cloud documentation: https://cloud.google.com/load-balancing/docs/backend-service **Terrafrom Mappings:** -- `google_compute_backend_service.name` + * `google_compute_backend_service.name` + * `google_compute_region_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`~~ +* `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-health-check`](/sources/gcp/Types/gcp-compute-health-check) + +A backend service is required to reference one or more Health Checks. These determine the health of each backend target and whether traffic should be sent to it. + +### [`gcp-compute-instance`](/sources/gcp/Types/gcp-compute-instance) + +Individual VM instances receive traffic indirectly through a backend service when they belong to an instance group or unmanaged instance list that the backend service uses. + +### [`gcp-compute-instance-group`](/sources/gcp/Types/gcp-compute-instance-group) + +Managed or unmanaged Instance Groups are the most common type of backend that a backend service points to. The group’s VMs are the actual targets for load-balanced traffic. + ### [`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. +Backends referenced by a backend service must reside in a specific VPC network; therefore the backend service is effectively bound to that network and its associated subnets and firewall rules. + +### [`gcp-compute-network-endpoint-group`](/sources/gcp/Types/gcp-compute-network-endpoint-group) + +Network Endpoint Groups (NEGs) can be configured as backends of a backend service to route traffic to endpoints such as containers, serverless services, or on-premises resources. ### [`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. +A backend service can optionally attach a Cloud Armor Security Policy to enforce L7 firewall rules, rate limiting, and other protective measures on incoming traffic before it reaches the backends. \ No newline at end of file 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 index 6b34dc83..e5278591 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-disk.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-disk.md @@ -3,37 +3,44 @@ 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 +A GCP Compute Disk—formally known as a Persistent Disk—is block-level storage that can be attached to Google Compute Engine virtual machine (VM) instances. Disks may be zonal or regional, support features such as snapshots, replication, and Customer-Managed Encryption Keys (CMEK), and can be resized or detached without data loss. Official documentation: https://cloud.google.com/compute/docs/disks **Terrafrom Mappings:** -- `google_compute_disk.name` + * `google_compute_disk.name` ## Supported Methods -- `GET`: Get GCP Compute Disk by "gcp-compute-disk-name" -- `LIST`: List all GCP Compute Disk items -- ~~`SEARCH`~~ +* `GET`: Get GCP Compute Disk by "gcp-compute-disk-name" +* `LIST`: List all GCP Compute Disk items +* ~~`SEARCH`~~ ## Possible Links +### [`gcp-cloud-kms-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) + +Indicates the specific Cloud KMS key version used when the disk is encrypted with a customer-managed encryption key. + ### [`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. +For regional or replicated disks, the resource records the relationship to its source or replica peer disk. ### [`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. +Shows the image from which the disk was created, or images that have been built from this 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. +Lists the VM instances to which the disk is currently attached or has been attached historically. ### [`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. +Captures the association between the disk and any instant snapshots taken for rapid backup or restore operations. ### [`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. +Represents traditional snapshots for the disk, enabling point-in-time recovery or disk cloning. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +If disk snapshots or images are exported to Cloud Storage, this link records the destination bucket holding those exports. \ No newline at end of file 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 index 5877253d..23bcdda3 100644 --- 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 @@ -3,15 +3,15 @@ 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 +A **Compute External VPN Gateway** is a Google Cloud resource that represents a customer-managed VPN appliance that resides outside of Google’s network (for example, in an on-premises data centre or another cloud). By defining one or more external interface IP addresses and an associated redundancy type, it tells Cloud VPN (HA VPN or Classic VPN) where to terminate its tunnels. In other words, the resource is the “remote end” of a Cloud VPN connection, allowing Google Cloud to establish secure IPSec tunnels to external infrastructure. +For further details, see the official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/externalVpnGateways **Terrafrom Mappings:** -- `google_compute_external_vpn_gateway.name` + * `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`~~ +* `GET`: Get a gcp-compute-external-vpn-gateway by its "name" +* `LIST`: List all gcp-compute-external-vpn-gateway +* ~~`SEARCH`~~ \ No newline at end of file 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 index 074bee1a..43f3c50b 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-firewall.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-firewall.md @@ -3,24 +3,29 @@ 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. +A Google Cloud VPC firewall rule controls inbound and outbound traffic to and from the virtual machine (VM) instances that are attached to a particular VPC network. Each rule specifies a direction, priority, action (allow or deny), protocol and port list, and a target (network tags or service accounts). Rules are stateful and are evaluated before traffic reaches any instance, allowing you to centrally enforce network security policy across your workloads. +Official documentation: https://cloud.google.com/vpc/docs/firewalls **Terrafrom Mappings:** -- `google_compute_firewall.name` + * `google_compute_firewall.name` ## Supported Methods -- `GET`: Get a gcp-compute-firewall by its "name" -- `LIST`: List all gcp-compute-firewall -- ~~`SEARCH`~~ +* `GET`: Get a gcp-compute-firewall by its "name" +* `LIST`: List all gcp-compute-firewall +* `SEARCH`: Search for firewalls by network tag. The query is a plain network tag name. ## Possible Links +### [`gcp-compute-instance`](/sources/gcp/Types/gcp-compute-instance) + +Firewall rules apply to VM instances that match their target criteria (network tags or service accounts). Therefore, an instance is linked to the firewall rules that currently govern the traffic it may send or receive. + ### [`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. +Every firewall rule is created within a specific VPC network. The rule only affects resources that are attached to that network, so it is linked to its parent network resource. ### [`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. +Firewall rules can target VM instances by the service account they are running as. When a rule uses the `target_service_accounts` field, it is related to those IAM service accounts. \ No newline at end of file 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 index 9d622a78..7a5dd132 100644 --- 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 @@ -3,29 +3,49 @@ 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 +A GCP Compute Forwarding Rule defines how incoming packets are handled within Google Cloud. It binds an IP address, protocol and (optionally) port range to a specific target resource such as a backend service, target proxy or target pool. Forwarding rules underpin both external and internal load-balancing solutions and can be either regional or global in scope. +For full details see the official documentation: https://cloud.google.com/load-balancing/docs/forwarding-rule-concepts. **Terrafrom Mappings:** -- `google_compute_forwarding_rule.name` + * `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`~~ +* `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. +The forwarding rule may specify a backend service as its target (for example, when configuring an Internal TCP/UDP Load Balancer or External HTTP(S) Load Balancer). + +### [`gcp-compute-forwarding-rule`](/sources/gcp/Types/gcp-compute-forwarding-rule) + +This represents the same forwarding-rule resource; Overmind links to it so that self-references or associations between global and regional rules can be tracked. ### [`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. +For internal forwarding rules, the rule is created inside a specific VPC network; the link identifies that parent network. + +### [`gcp-compute-public-delegated-prefix`](/sources/gcp/Types/gcp-compute-public-delegated-prefix) + +If the rule’s IP address is allocated from a delegated public prefix, it will be linked to that prefix to show the allocation source. ### [`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. +Internal forwarding rules also reference the subnetwork from which their internal IP address is drawn. + +### [`gcp-compute-target-http-proxy`](/sources/gcp/Types/gcp-compute-target-http-proxy) + +External HTTP Load Balancer forwarding rules target an HTTP proxy, so the rule links to the relevant `target-http-proxy` resource. + +### [`gcp-compute-target-https-proxy`](/sources/gcp/Types/gcp-compute-target-https-proxy) + +External HTTPS Load Balancer forwarding rules target an HTTPS proxy; this link identifies that proxy. + +### [`gcp-compute-target-pool`](/sources/gcp/Types/gcp-compute-target-pool) + +Legacy Network Load Balancer forwarding rules can point directly to a target pool; the link shows which pool receives the traffic. \ No newline at end of file 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 index 85788cc6..ee1e3364 100644 --- 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 @@ -3,21 +3,29 @@ 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 +A **Compute Global Address** in Google Cloud Platform is a statically-reserved IP address that is reachable from, or usable across, all regions. It can be external (used, for example, by a global HTTP(S) load balancer) or internal (used by regional resources that require a routable, private global IP). Reserving the address ensures it does not change while it is in use, and allows it to be assigned to resources at creation time or later. +Official documentation: https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address **Terrafrom Mappings:** -- `google_compute_global_address.name` + * `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`~~ +* `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. +A global address may be bound to a specific VPC network when it is reserved as an internal global IP. Overmind links the address to the `gcp-compute-network` so you can see in which network the address is routable and assess overlapping CIDR or routing risks. + +### [`gcp-compute-public-delegated-prefix`](/sources/gcp/Types/gcp-compute-public-delegated-prefix) + +If the address is carved out of a public delegated prefix that your project controls, Overmind links it to that `gcp-compute-public-delegated-prefix` to show the parent block and enable checks for exhaustion or mis-allocation. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +For internal global addresses that are further scoped to a particular subnetwork, Overmind establishes a link to the `gcp-compute-subnetwork` so you can trace which subnet’s routing table and firewall rules apply to traffic destined for the address. \ No newline at end of file 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 index fae8d4a0..576441b4 100644 --- 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 @@ -3,29 +3,32 @@ 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 +A Google Cloud Compute Global Forwarding Rule defines a single anycast virtual IP address that routes incoming traffic at the global level to a specified target (such as an HTTP(S) proxy, SSL proxy or TCP proxy) or, for internal load balancing, directly to a backend service. It is the entry-point resource for most external HTTP(S) and proxy load balancers and for internal global load balancers. For full details see the Google Cloud documentation: https://cloud.google.com/load-balancing/docs/forwarding-rule-concepts **Terrafrom Mappings:** -- `google_compute_global_forwarding_rule.name` + * `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`~~ +* `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. +When the forwarding rule is created for an internal global load balancer, it references a backend service directly; the rule’s traffic is delivered to the backends listed in that service. Analysing this link lets Overmind trace traffic paths from the VIP to the actual instances or endpoints. ### [`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. +Internal global forwarding rules must be attached to a specific VPC network. Linking to the network resource reveals which project-wide connectivity domain the VIP belongs to and helps surface risks such as unintended exposure to peered networks. ### [`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. +If the forwarding rule is internal, it is scoped to a particular subnetwork. Understanding this relationship identifies the IP range in which the virtual IP lives and highlights segmentation or overlapping-CIDR issues. + +### [`gcp-compute-target-http-proxy`](/sources/gcp/Types/gcp-compute-target-http-proxy) + +For external HTTP(S), SSL or TCP proxy load balancers, the forwarding rule points to a target proxy resource. The proxy terminates the client connection before forwarding to backend services. Linking these resources enables Overmind to trace configuration chains and detect misconfigurations such as SSL policy mismatches or missing backends. \ No newline at end of file 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 index 61efaaff..0550f69e 100644 --- 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 @@ -3,15 +3,16 @@ 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 +A GCP Compute Health Check is a Google Cloud resource that periodically probes virtual machine instances or endpoints to decide whether they are fit to receive production traffic. The check runs from the Google-managed control plane using protocols such as TCP, SSL, HTTP(S), HTTP/2 or gRPC, and compares the response to thresholds you configure (e.g. response code, timeout, healthy/unhealthy counts). Backend services, target pools and managed instance groups use the resulting health status to route requests only to healthy instances and to trigger autoscaling or fail-over behaviour. Health checks come in global and regional flavours, aligning with global and regional load balancers respectively. +Official documentation: https://cloud.google.com/load-balancing/docs/health-checks **Terrafrom Mappings:** -- `google_compute_health_check.name` + * `google_compute_health_check.name` + * `google_compute_region_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`~~ +* `GET`: Get GCP Compute Health Check by "gcp-compute-health-check-name" +* `LIST`: List all GCP Compute Health Check items +* ~~`SEARCH`~~ \ No newline at end of file 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 index 095d2e51..bf563c5c 100644 --- 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 @@ -3,16 +3,15 @@ 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 +A GCP Compute HTTP Health Check is a globally scoped resource that periodically sends HTTP requests to a specified port and path on your instances or endpoints to verify that they are responding correctly. Load balancers, managed instance groups and other Google Cloud services use the results of these checks to decide whether traffic should be routed to a given backend. Each check can be customised with parameters such as the request path, host header, check interval, timeout, and healthy/unhealthy thresholds. +For further details see the official documentation: https://cloud.google.com/compute/docs/load-balancing/health-checks#http-health-checks **Terrafrom Mappings:** -- `google_compute_http_health_check.name` + * `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`~~ +* `GET`: Get a gcp-compute-http-health-check by its "name" +* `LIST`: List all gcp-compute-http-health-check +* ~~`SEARCH`~~ \ No newline at end of file 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 index 77040607..ae515210 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-image.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-image.md @@ -3,15 +3,45 @@ 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 +A Google Cloud Compute Image is a read-only template that contains a boot disk configuration (including the operating system and any installed software) which can be used to create new persistent disks or VM instances. Images may be publicly provided by Google, published by third-party vendors, or built privately within your own project. They support features such as image families, deprecation, and customer-managed encryption keys (CMEK). +For full details see the official documentation: https://cloud.google.com/compute/docs/images **Terrafrom Mappings:** -- `google_compute_image.name` + * `google_compute_image.name` ## Supported Methods -- `GET`: Get GCP Compute Image by "gcp-compute-image-name" -- `LIST`: List all GCP Compute Image items -- ~~`SEARCH`~~ +* `GET`: Get GCP Compute Image by "gcp-compute-image-name" +* `LIST`: List all GCP Compute Image items +* `SEARCH`: Search for GCP Compute Image by "gcp-compute-image-family" + +## Possible Links + +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If the image is protected with a customer-managed encryption key (CMEK), Overmind links the image to the Cloud KMS Crypto Key that encrypts its contents. + +### [`gcp-cloud-kms-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) + +When CMEK protection specifies an explicit key version, the image is linked to that exact Crypto Key Version so you can trace roll-overs or revocations that might affect instance bootability. + +### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) + +Images can be created from existing persistent disks, and new disks can be created from an image. Overmind therefore links images to the disks that serve as their source or to the disks that have been instantiated from them. + +### [`gcp-compute-image`](/sources/gcp/Types/gcp-compute-image) + +Images belonging to the same image family or derived from one another (for example, when rolling a new version) are cross-linked so you can understand upgrade paths and deprecations within a family. + +### [`gcp-compute-snapshot`](/sources/gcp/Types/gcp-compute-snapshot) + +An image may be built from one or more snapshots of a disk, and snapshots can be exported from an image. Overmind links images to the snapshots that contributed to, or were generated from, them. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Access to create, deprecate or use an image is controlled through IAM roles. Overmind shows the service accounts that have permissions on the image, helping you assess who can launch VMs from it. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +During import or export operations, raw disk files are stored in Cloud Storage. Overmind links an image to the Storage Buckets that hosted its source or export objects, enabling you to trace data residency and clean-up unused artefacts. \ No newline at end of file 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 index bbe86356..f4e24bbb 100644 --- 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 @@ -3,32 +3,37 @@ 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. +A Compute Instance Group Manager (IGM) is the control plane object for a Managed Instance Group in Google Cloud Platform. It is responsible for creating, deleting, and maintaining a homogeneous fleet of Compute Engine virtual machines according to a declarative configuration such as target size, instance template and update policy. Because the manager continually reconciles the group’s actual state with the desired state, it underpins features like rolling updates, auto-healing and autoscaling. +Official documentation: https://cloud.google.com/compute/docs/instance-groups/creating-groups-of-managed-instances **Terrafrom Mappings:** -- `google_compute_instance_group_manager.name` + * `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`~~ +* `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. +An Autoscaler resource can target a Managed Instance Group via its Instance Group Manager, dynamically increasing or decreasing the group’s size based on utilisation metrics or schedules. + +### [`gcp-compute-health-check`](/sources/gcp/Types/gcp-compute-health-check) + +Within an auto-healing policy the Instance Group Manager references one or more Health Check resources to decide when individual instances should be recreated. ### [`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. +The Instance Group Manager encapsulates and manages an underlying (managed) Instance Group resource that represents the actual collection of VM instances. ### [`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. +The manager uses an Instance Template to define the configuration (machine type, disks, metadata, etc.) of every VM it creates in the group. ### [`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. +For legacy network load balancing, an Instance Group Manager can be configured to automatically add or remove its instances from a Target Pool, enabling them to receive traffic from a forwarding rule. \ No newline at end of file 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 index ee62276d..83c9e203 100644 --- 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 @@ -3,25 +3,25 @@ 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 +A Google Cloud Compute Instance Group is a logical collection of Virtual Machine (VM) instances running on Google Compute Engine that are treated as a single entity for deployment, scaling and load-balancing purposes. Instance groups can be managed (all VMs created from a common template and automatically kept in the desired size/state) or unmanaged (a user-assembled set of individual VMs). They are commonly used behind load balancers to provide highly available, horizontally scalable services. +For full details see the official Google Cloud documentation: https://cloud.google.com/compute/docs/instance-groups **Terrafrom Mappings:** -- `google_compute_instance_group.name` + * `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`~~ +* `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. +Every VM in an Instance Group must be attached to a VPC network. Overmind therefore links a Compute Instance Group to the Compute Network that provides its underlying connectivity, enabling you to trace how network-level policies or mis-configurations might affect the availability of the workload hosted by 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. +Within a given VPC network, all VMs in the Instance Group reside in a specific subnetwork. Overmind links the Instance Group to that Subnetwork so you can understand IP address allocation, regional placement and any subnet-specific firewall rules that could impact the instances. \ No newline at end of file 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 index fd742bd5..43af8bf9 100644 --- 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 @@ -3,61 +3,69 @@ 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. +A Google Cloud Compute Instance Template is a reusable description of the properties required to create a virtual machine (VM) instance. It encapsulates details such as machine type, boot image, disks, network interfaces, metadata, tags, and service-account settings. Once defined, the template can be used by users, managed instance groups, autoscalers, or other automation to create identically configured VMs at scale. Official documentation: https://cloud.google.com/compute/docs/instance-templates **Terrafrom Mappings:** -- `google_compute_instance_template.name` + * `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`~~ +* `GET`: Get a gcp-compute-instance-template by its "name" +* `LIST`: List all gcp-compute-instance-template +* `SEARCH`: Search for instance templates by network tag. The query is a plain network tag name. ## 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. +An instance template can reference a customer-managed encryption key (CMEK) from Cloud KMS to encrypt the persistent disks defined in 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. +Boot and additional persistent disks are specified inside the template. Any disk image or snapshot expanded into an actual persistent disk at instance-creation time will appear as a linked compute-disk resource. + +### [`gcp-compute-firewall`](/sources/gcp/Types/gcp-compute-firewall) + +The network tags set in the template are used by VMs launched from it. Firewall rules that target those tags therefore become effective for every instance derived from the template. ### [`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. +The template’s boot disk references a specific compute image (public, custom, or shared). This image is the source from which the VM’s root filesystem is created. ### [`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. +When a VM is launched using this template—either manually or by a managed instance group—the resulting resource is a compute-instance that maintains a provenance link back to 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. +Each network interface declared in the template must point to a VPC network, establishing the connectivity context for all future instances based on the template. ### [`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. +If node affinity is configured in the template, instances created from it will attempt to schedule onto the specified sole-tenant 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. +A template can include reservation affinity, causing newly created VMs to consume capacity from a specific Compute Engine reservation. + +### [`gcp-compute-route`](/sources/gcp/Types/gcp-compute-route) + +Although routes are defined at the network level, all VMs derived from the template inherit those routes through their attached network, so routing behaviour is indirectly influenced by the template. ### [`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. +If instances launched from the template are later attached to backend services that use Cloud Armor security policies, their traffic will be evaluated against those policies; tracing the link helps assess exposure. ### [`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. +The template may specify a source snapshot instead of an image for one or more disks, resulting in disks that are restored from those snapshots at VM creation time. ### [`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. +For each network interface, the template can identify a specific subnetwork, dictating the IP range from which the instance will draw its primary internal address. ### [`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. +A service account can be attached in the template so that every VM started from it runs with the same IAM identity and associated OAuth scopes. \ No newline at end of file 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 index ffa3f837..92dd6a93 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance.md @@ -3,28 +3,64 @@ 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 +A Google Cloud Compute Engine instance is a virtual machine (VM) that runs on Google’s infrastructure. It provides configurable CPU, memory, disk and network resources so you can run workloads in a scalable, on-demand manner. For full details see the official documentation: https://cloud.google.com/compute/docs/instances. **Terrafrom Mappings:** -- `google_compute_instance.name` + * `google_compute_instance.name` ## Supported Methods -- `GET`: Get GCP Compute Instance by "gcp-compute-instance-name" -- `LIST`: List all GCP Compute Instance items -- ~~`SEARCH`~~ +* `GET`: Get GCP Compute Instance by "gcp-compute-instance-name" +* `LIST`: List all GCP Compute Instance items +* `SEARCH`: Search for GCP Compute Instance by "gcp-compute-instance-networkTag" ## Possible Links +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +If the instance’s boot or data disks are encrypted with customer-managed encryption keys (CMEK), it references a Cloud KMS crypto key. + +### [`gcp-cloud-kms-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) + +A specific version of the KMS key may be recorded when CMEK encryption is enabled on the instance’s disks. + ### [`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. +Boot and additional persistent disks are attached to the instance; these disks back the VM’s storage. + +### [`gcp-compute-firewall`](/sources/gcp/Types/gcp-compute-firewall) + +Firewall rules that target the instance’s network tags or service account control inbound and outbound traffic for the VM. + +### [`gcp-compute-image`](/sources/gcp/Types/gcp-compute-image) + +The instance’s boot disk is created from a Compute Engine image, capturing the operating system and initial state. + +### [`gcp-compute-instance-group-manager`](/sources/gcp/Types/gcp-compute-instance-group-manager) + +When the VM is part of a managed instance group (MIG), the group manager is responsible for creating, deleting and updating the instance. + +### [`gcp-compute-instance-template`](/sources/gcp/Types/gcp-compute-instance-template) + +Instances launched via a template inherit machine type, disks, metadata and network settings defined in that template. ### [`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. +Every network interface on the instance is connected to a VPC network, determining the VM’s reachable address space. + +### [`gcp-compute-route`](/sources/gcp/Types/gcp-compute-route) + +Routes in the attached VPC network dictate how the instance’s traffic is forwarded; some routes may apply only to instances with specific tags. + +### [`gcp-compute-snapshot`](/sources/gcp/Types/gcp-compute-snapshot) + +Snapshots can be taken from the instance’s persistent disks for backup or cloning purposes, creating a link between the VM and its snapshots. ### [`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. +Each network interface is placed within a subnetwork, assigning the instance its internal IP range and regional scope. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +An optional service account is attached to the instance, granting it IAM-scoped credentials to access Google APIs. \ No newline at end of file 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 index b26e582d..c513cd75 100644 --- 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 @@ -3,21 +3,21 @@ 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 +A GCP Compute Instant Snapshot is a point-in-time, crash-consistent copy of a Compute Engine persistent disk that is created almost instantaneously, permitting rapid backup, cloning, and disaster-recovery workflows. Instant snapshots can be used to restore a disk to the exact state it was in when the snapshot was taken or to create new disks that replicate that state. They differ from traditional snapshots primarily in the speed at which they are taken and restored. +Official documentation: https://cloud.google.com/compute/docs/disks/instant-snapshots **Terrafrom Mappings:** -- `google_compute_instant_snapshot.name` + * `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`~~ +* `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. +An instant snapshot is always sourced from an existing Compute Engine persistent disk. Therefore, each `gcp-compute-instant-snapshot` has a direct parent–child relationship with the `gcp-compute-disk` it captures, and Overmind links the snapshot back to the originating disk to surface dependency and recovery paths. \ No newline at end of file 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 index a8df0527..fec5aabe 100644 --- 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 @@ -3,33 +3,48 @@ 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 +A Google Cloud Compute Machine Image is a first-class resource that captures the full state of a virtual machine at a point in time, including all attached disks, metadata, instance properties, service-accounts, and network configuration. It can be used to recreate identical VMs quickly or share a golden template across projects and organisations. See the official documentation for full details: https://cloud.google.com/compute/docs/machine-images **Terrafrom Mappings:** -- `google_compute_machine_image.name` + * `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`~~ +* `GET`: Get GCP Compute Machine Image by "gcp-compute-machine-image-name" +* `LIST`: List all GCP Compute Machine Image items +* ~~`SEARCH`~~ ## Possible Links +### [`gcp-cloud-kms-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) + +A machine image may be protected with customer-managed encryption keys (CMEK); when this option is used it references the specific Cloud KMS Crypto Key Version that encrypts the image data. + ### [`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. +The boot disk and any additional data disks attached to the source instance are incorporated into the machine image. When a new instance is created from the machine image, new persistent disks are instantiated from these definitions. + +### [`gcp-compute-image`](/sources/gcp/Types/gcp-compute-image) + +Within a machine image the boot disk is ultimately based on a Compute Image. Thus the machine image indirectly depends on, and records, the image that was used to build the source VM. ### [`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. +A machine image is created from a source Compute Instance and can in turn be used to launch new instances that replicate the captured configuration. ### [`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. +Network interface settings, including the VPC network IDs, are stored in the machine image so that any VM instantiated from it can attach to the same or equivalent networks. + +### [`gcp-compute-snapshot`](/sources/gcp/Types/gcp-compute-snapshot) + +Internally, Google Cloud may use snapshots of the instance’s disks when building the machine image. Conversely, users can export disks from a machine image as individual snapshots. ### [`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. +The machine image stores the exact subnetwork configuration of each NIC, allowing recreated VMs to provision themselves in the same subnetworks. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Service accounts attached to the source instance are recorded in the machine image; any VM launched from the image inherits those service account bindings unless overridden. \ No newline at end of file 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 index 4377abfc..6fe662e9 100644 --- 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 @@ -3,32 +3,32 @@ 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/. +A Google Cloud Platform Compute Network Endpoint Group (NEG) is a collection of network endpoints—such as VM NICs, container pods, Cloud Run services, or Cloud Functions—that can be treated as a single backend target by Load Balancing and Service Directory. NEGs give fine-grained control over which exact endpoints receive traffic and allow serverless or hybrid back-ends to participate in layer-4/7 load balancing. See the official documentation for full details: https://cloud.google.com/load-balancing/docs/negs. **Terrafrom Mappings:** -- `google_compute_network_endpoint_group.name` + * `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`~~ +* `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. +A serverless NEG can reference a specific Cloud Function. Overmind therefore links the NEG to the underlying `gcp-cloud-functions-function` it represents, showing which function will receive traffic through the load balancer. ### [`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`. +Zonal and regional NEGs are created inside a particular VPC network. The link indicates the network context in which the endpoints exist, helping to surface routing and firewall considerations. ### [`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`. +When a NEG is scoped to a subnetwork (for example for VM or GKE pod endpoints), Overmind links it to that subnetwork so you can trace how traffic enters specific IP ranges. ### [`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`. +Serverless NEGs can point to Cloud Run services. This link shows which `gcp-run-service` is exposed through the NEG and subsequently through any HTTP(S) load balancer. \ No newline at end of file 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 index 368d7337..a0d2fb75 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network.md @@ -3,25 +3,24 @@ 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. +A Google Cloud Platform (GCP) Compute Network—commonly called a Virtual Private Cloud (VPC) network—provides the fundamental isolation and IP address space in which all other networking resources (subnetworks, routes, firewall rules, VPNs, etc.) are created. It is a global resource that spans all regions in a project, allowing workloads to communicate securely inside Google’s backbone and to the internet where required. For a full description see the official documentation: https://cloud.google.com/vpc/docs/vpc **Terrafrom Mappings:** -- `google_compute_network.name` + * `google_compute_network.name` ## Supported Methods -- `GET`: Get a gcp-compute-network by its "name" -- `LIST`: List all gcp-compute-network -- ~~`SEARCH`~~ +* `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. +A Compute Network can be peered with, or shared to, another Compute Network. Overmind records these peer or shared-VPC relationships by linking one `gcp-compute-network` item to the other(s). ### [`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. +Every subnetwork is created inside exactly one VPC network. Overmind therefore links each `gcp-compute-subnetwork` back to its parent `gcp-compute-network`, and conversely shows the network’s collection of subnetworks. \ No newline at end of file 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 index a5493f53..1b32823d 100644 --- 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 @@ -3,16 +3,15 @@ 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 +A GCP Compute Node Group is a managed collection of sole-tenant nodes that are all created from the same node template. These groups allow you to provision and administer dedicated physical servers for your Compute Engine virtual machines, giving you fine-grained control over workload isolation, hardware affinity, licensing, and maintenance windows. For a detailed explanation, see the official Google Cloud documentation: https://cloud.google.com/compute/docs/nodes. **Terrafrom Mappings:** -- `google_compute_node_group.name` -- `google_compute_node_template.name` + * `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" +* `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" \ No newline at end of file diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-template.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-template.md new file mode 100644 index 00000000..7ebe1ace --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-template.md @@ -0,0 +1,23 @@ +--- +title: GCP Compute Node Template +sidebar_label: gcp-compute-node-template +--- + +A GCP Compute Node Template is a reusable description of the hardware configuration and host maintenance policies that will be applied to one or more Sole-Tenant Nodes in Google Cloud. The template specifies attributes such as CPU platform, virtual CPU count, memory, node affinity labels, and automatic restart behaviour. When you later create a Node Group, the group references a single Node Template, ensuring that every node in the group is created with an identical shape. +For a full specification of the resource, see the official Google Cloud documentation: https://cloud.google.com/compute/docs/nodes/sole-tenant-nodes + +**Terrafrom Mappings:** + + * `google_compute_node_template.name` + +## Supported Methods + +* `GET`: Get GCP Compute Node Template by "gcp-compute-node-template-name" +* `LIST`: List all GCP Compute Node Template items +* ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-node-group`](/sources/gcp/Types/gcp-compute-node-group) + +A GCP Compute Node Group consumes a single Node Template. Overmind creates a link from a node group back to the template it references so that you can assess how changes to the template (for example, switching CPU platforms) will affect every node that belongs to the group. \ No newline at end of file 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 index 11c1ab06..1f1693c5 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-project.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-project.md @@ -3,21 +3,31 @@ 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 +A Google Cloud Project is the fundamental organisational unit in Google Cloud Platform. It acts as a logical container for all your Google Cloud resources, identity and access management (IAM) policies, APIs, quotas and billing information. Every resource – from virtual machines to service accounts – is created in exactly one project, and project-level settings (such as audit logging, labels and network host project status) govern how those resources operate. See the official documentation for full details: https://cloud.google.com/resource-manager/docs/creating-managing-projects + +**Terrafrom Mappings:** + + * `google_project.project_id` + * `google_compute_shared_vpc_host_project.project` + * `google_compute_shared_vpc_service_project.service_project` + * `google_compute_shared_vpc_service_project.host_project` + * `google_project_iam_binding.project` + * `google_project_iam_member.project` + * `google_project_iam_policy.project` + * `google_project_iam_audit_config.project` ## Supported Methods -- `GET`: Get a gcp-compute-project by its "name" -- ~~`LIST`~~ -- ~~`SEARCH`~~ +* `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. +Service accounts are identities that live inside a project. Overmind links a gcp-iam-service-account to its parent gcp-compute-project to show which project owns and governs the credentials and IAM permissions of that service account. ### [`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. +Every Cloud Storage bucket is created within a specific project. Overmind establishes a link from a gcp-storage-bucket back to its gcp-compute-project so you can trace ownership, billing and IAM inheritance for the bucket. \ No newline at end of file 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 index 32dd6369..05371dda 100644 --- 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 @@ -3,25 +3,25 @@ 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 +A Public Delegated Prefix is a regional IPv4 or IPv6 address range that you reserve from Google Cloud and can then subdivide and delegate to other projects, VPC networks, or Private Service Connect service attachments. It allows you to keep ownership of the parent prefix while giving consumers controlled use of sub-prefixes, simplifying address management and avoiding manual peering or routing configurations. +For full details, see the official documentation: https://cloud.google.com/vpc/docs/create-pdp **Terrafrom Mappings:** -- `google_compute_public_delegated_prefix.id` + * `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). +* `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. +This prefix belongs to and is created within a specific Google Cloud project; the link points from the Public Delegated Prefix to its parent project. ### [`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. +A parent Public Delegated Prefix can be linked to child delegated sub-prefixes (or vice-versa) to represent hierarchy and inheritance of the IP space. \ No newline at end of file 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 deleted file mode 100644 index 4c64e0d0..00000000 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-region-backend-service.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -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 index 345b2e62..c5b90832 100644 --- 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 @@ -3,20 +3,20 @@ 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. +A Compute Region Commitment in Google Cloud Platform (GCP) represents a contractual agreement to purchase a certain amount of vCPU, memory, GPUs or local SSD capacity within a specific region for one or three years. In exchange for this up-front commitment, you receive a discounted hourly rate for the covered resources, regardless of whether the capacity is actually in use. Commitments are created per-project and per-region, and the discount automatically applies to any eligible VM instances running in that region. For full details see the official documentation: https://cloud.google.com/compute/docs/instances/signing-up-committed-use-discounts **Terrafrom Mappings:** -- `google_compute_region_commitment.name` + * `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`~~ +* `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. +Reservations and commitments often work together: a reservation guarantees that capacity is available, while a commitment provides a discount for that capacity. When Overmind discovers a region commitment it links it to any compute reservations in the same project and region so you can see both the cost commitment and the capacity guarantee in one place. \ No newline at end of file diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-regional-instance-group-manager.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-regional-instance-group-manager.md new file mode 100644 index 00000000..286bf0cb --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-regional-instance-group-manager.md @@ -0,0 +1,39 @@ +--- +title: GCP Compute Regional Instance Group Manager +sidebar_label: gcp-compute-regional-instance-group-manager +--- + +A Google Cloud Compute Regional Instance Group Manager (RIGM) is a control plane resource that creates, deletes, updates and monitors a homogeneous set of virtual machine (VM) instances that are distributed across two or more zones within the same region. By using a RIGM you gain automated rolling updates, proactive auto-healing and the ability to spread workload across zones for higher availability. +Official documentation: https://cloud.google.com/compute/docs/instance-groups/creating-groups-of-managed-instances#regional + +**Terrafrom Mappings:** + + * `google_compute_region_instance_group_manager.name` + +## Supported Methods + +* `GET`: Get GCP Compute Regional Instance Group Manager by "gcp-compute-regional-instance-group-manager-name" +* `LIST`: List all GCP Compute Regional Instance Group Manager items +* ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-autoscaler`](/sources/gcp/Types/gcp-compute-autoscaler) + +A regional instance group manager can be linked to an Autoscaler resource that dynamically adjusts the number of VM instances in the managed group based on load, schedules or custom metrics. + +### [`gcp-compute-health-check`](/sources/gcp/Types/gcp-compute-health-check) + +Health checks are referenced by the RIGM to perform auto-healing; instances that fail the configured health check are recreated automatically. + +### [`gcp-compute-instance-group`](/sources/gcp/Types/gcp-compute-instance-group) + +The RIGM creates and controls a Regional Managed Instance Group. This underlying instance group is where the actual VM instances live and where traffic is balanced. + +### [`gcp-compute-instance-template`](/sources/gcp/Types/gcp-compute-instance-template) + +Every RIGM points to an Instance Template that defines the machine type, boot disk, metadata and other properties used when new VM instances are instantiated. + +### [`gcp-compute-target-pool`](/sources/gcp/Types/gcp-compute-target-pool) + +For legacy network load balancing, a RIGM can register its instances with a Target Pool so that traffic from a network load balancer is distributed across the managed instances. \ No newline at end of file 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 index 446cca00..ec37581b 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-reservation.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-reservation.md @@ -3,20 +3,20 @@ 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. +A GCP Compute Reservation is a zonal capacity-planning resource that lets you pre-allocate Compute Engine virtual machine capacity so that it is always available when your workloads need it. By creating a reservation you can guarantee that the required number and type of vCPUs, memory and accelerators are held for your project in a particular zone, avoiding scheduling failures during peaks or regional outages. For full details, see the official Google Cloud documentation: https://cloud.google.com/compute/docs/instances/reserving-zonal-resources **Terrafrom Mappings:** -- `google_compute_reservation.name` + * `google_compute_reservation.name` ## Supported Methods -- `GET`: Get GCP Compute Reservation by "gcp-compute-reservation-name" -- `LIST`: List all GCP Compute Reservation items -- ~~`SEARCH`~~ +* `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. +Reservations guarantee capacity, while regional commitments provide sustained-use discounts for that capacity. A reservation created in a zone may be covered by, or contribute to the utilisation of, a regional commitment in the same region, so analysing the commitment alongside the reservation reveals both availability and cost-optimisation aspects of the deployment. \ No newline at end of file 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 index 3cbe6af4..66164775 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-route.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-route.md @@ -3,29 +3,33 @@ 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 +A **GCP Compute Route** is a routing rule attached to a Google Cloud Virtual Private Cloud (VPC) network that determines how packets are forwarded from instances towards their destinations. Each route contains a destination CIDR block and a single next-hop target, such as an instance, VPN tunnel, gateway or internal load-balancer forwarding rule. Routes can be either system-generated (e.g. subnet and peering routes) or user-defined to control custom traffic flows, enforce security boundaries or implement hybrid-connectivity scenarios. +Official documentation: https://cloud.google.com/vpc/docs/routes **Terrafrom Mappings:** -- `google_compute_route.name` + * `google_compute_route.name` ## Supported Methods -- `GET`: Get a gcp-compute-route by its "name" -- `LIST`: List all gcp-compute-route -- ~~`SEARCH`~~ +* `GET`: Get a gcp-compute-route by its "name" +* `LIST`: List all gcp-compute-route +* `SEARCH`: Search for routes by network tag. The query is a plain network tag name. ## Possible Links +### [`gcp-compute-forwarding-rule`](/sources/gcp/Types/gcp-compute-forwarding-rule) + +A route may specify an internal TCP/UDP load balancer (ILB) forwarding rule as its `nextHopIlb`, so the route is linked to the forwarding rule that receives the traffic. + ### [`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. +When `nextHopInstance` is used, the route points to a specific Compute Engine instance that acts as a gateway. Instances are therefore linked as potential next hops for 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. +Every route is created inside exactly one VPC network, referenced by the `network` field. The relationship ties the route to the network whose traffic it influences. ### [`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. +If `nextHopVpnTunnel` is set, the route forwards matching traffic into a Cloud VPN tunnel. The route is consequently linked to the VPN tunnel resource it targets. \ No newline at end of file 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 index 107de7f2..5dd98194 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-router.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-router.md @@ -3,29 +3,28 @@ 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 +A Google Cloud Compute Router is a fully distributed and managed Border Gateway Protocol (BGP) routing service that dynamically exchanges routes between your Virtual Private Cloud (VPC) network and on-premises or cloud networks connected via VPN or Cloud Interconnect. By advertising only the necessary prefixes, it enables highly available, scalable, and policy-driven traffic engineering without the need to run or maintain your own routing appliances. See the official documentation for full details: https://cloud.google.com/network-connectivity/docs/router **Terrafrom Mappings:** -- `google_compute_router.id` + * `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). +* `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. +A Compute Router is created inside a specific VPC network and advertises routes for that network; therefore it is directly linked to the gcp-compute-network resource in which it resides. ### [`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. +Subnets within the parent VPC network can have their routes propagated or learned via the Compute Router, especially when using dynamic routing modes; this establishes an indirect but important relationship with each gcp-compute-subnetwork. ### [`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. +When Cloud VPN is configured in dynamic mode, the VPN tunnel relies on a Compute Router to exchange BGP routes with the peer gateway, making the tunnel dependent on, and logically linked to, the corresponding gcp-compute-vpn-tunnel resource. \ No newline at end of file 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 index e4013647..12e8b263 100644 --- 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 @@ -3,16 +3,15 @@ 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 +A GCP Compute Security Policy represents a Google Cloud Armor security policy that you configure to protect your applications and services from malicious or unwanted traffic. Each policy is made up of an ordered list of rules that allow, deny, or rate-limit requests based on layer-3/4 characteristics or custom layer-7 expressions. Security policies can be associated with external Application Load Balancers, Cloud CDN, and other HTTP(S)-based backend services, enabling centralised, declarative control over inbound traffic behaviour. +For full details, see the official Google documentation: https://cloud.google.com/compute/docs/reference/rest/v1/securityPolicies **Terrafrom Mappings:** -- `google_compute_security_policy.name` + * `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`~~ +* `GET`: Get GCP Compute Security Policy by "gcp-compute-security-policy-name" +* `LIST`: List all GCP Compute Security Policy items +* ~~`SEARCH`~~ \ No newline at end of file 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 index 2dba8fa9..d06d43a5 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-snapshot.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-snapshot.md @@ -3,25 +3,29 @@ 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 +A **GCP Compute Snapshot** is a point-in-time, incremental backup of a Compute Engine persistent or regional disk. Snapshots can be stored in multiple regions, encrypted with customer-managed keys, and used to create new disks, thereby providing a simple mechanism for backup, disaster recovery and environment cloning. +Official documentation: https://cloud.google.com/compute/docs/disks/create-snapshots **Terrafrom Mappings:** -- `google_compute_snapshot.name` + * `google_compute_snapshot.name` ## Supported Methods -- `GET`: Get GCP Compute Snapshot by "gcp-compute-snapshot-name" -- `LIST`: List all GCP Compute Snapshot items -- ~~`SEARCH`~~ +* `GET`: Get GCP Compute Snapshot by "gcp-compute-snapshot-name" +* `LIST`: List all GCP Compute Snapshot items +* ~~`SEARCH`~~ ## Possible Links +### [`gcp-cloud-kms-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) + +If the snapshot is encrypted with a customer-managed encryption key (CMEK), it references the specific Cloud KMS CryptoKeyVersion that holds the key material. Overmind links the snapshot to that key version so you can trace encryption dependencies and confirm key rotation policies. + ### [`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. +Every snapshot originates from a source disk. This link shows which Compute Engine disk (zonal or regional) was used to create the snapshot, letting you assess blast radius and recovery workflows. ### [`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. +An instant snapshot is a fast, crash-consistent capture that can later be converted into a regular snapshot. When such a conversion occurs, Overmind links the resulting standard snapshot to its originating instant snapshot, giving visibility into the lineage of your backups. \ No newline at end of file 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 index 350c08be..ce0c95fc 100644 --- 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 @@ -3,15 +3,14 @@ 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 +A **Google Compute SSL Certificate** represents an SSL certificate resource that can be attached to Google Cloud load-balancers to provide encrypted (HTTPS or SSL proxy) traffic termination. It stores the public certificate and its corresponding private key, enabling Compute Engine and Cloud Load Balancing to serve traffic securely on the specified domains. Certificates can be self-managed (you upload the PEM-encoded certificate and key) or Google-managed (Google provisions and renews them automatically). Full details are available in the official documentation: [Google Compute Engine – SSL certificates](https://cloud.google.com/compute/docs/reference/rest/v1/sslCertificates). **Terrafrom Mappings:** -- `google_compute_ssl_certificate.name` + * `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`~~ +* `GET`: Get a gcp-compute-ssl-certificate by its "name" +* `LIST`: List all gcp-compute-ssl-certificate +* ~~`SEARCH`~~ \ No newline at end of file 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 index 020ac502..37ef410f 100644 --- 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 @@ -3,15 +3,15 @@ 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). +A Google Cloud Compute **SSL Policy** specifies the minimum TLS protocol version and the set of supported cipher suites that HTTPS or SSL-proxy load balancers are allowed to use when negotiating SSL/TLS with clients. By attaching an SSL Policy to a target HTTPS proxy or target SSL proxy, you can enforce stronger security standards, ensure compliance, and gradually deprecate outdated encryption algorithms without disrupting traffic. +For detailed information, refer to the official Google Cloud documentation: https://cloud.google.com/load-balancing/docs/ssl-policies-concepts. **Terrafrom Mappings:** -- `google_compute_ssl_policy.name` + * `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`~~ +* `GET`: Get a gcp-compute-ssl-policy by its "name" +* `LIST`: List all gcp-compute-ssl-policy +* ~~`SEARCH`~~ \ No newline at end of file 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 index 00e54145..3ab7fb97 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-subnetwork.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-subnetwork.md @@ -3,20 +3,24 @@ 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 +A GCP Compute Subnetwork is a regional, layer-3 virtual network segment that belongs to a single Google Cloud VPC network. It defines an internal RFC 1918 IP address range (primary and optional secondary ranges) from which VM instances, containers and other resources receive their internal IPs. Within each subnetwork you can enable or disable Private Google Access, set flow-log export settings, IPv6 configurations, and control access through firewall rules inherited from the parent VPC. For a comprehensive overview refer to the official documentation: https://cloud.google.com/vpc/docs/subnets. **Terrafrom Mappings:** -- `google_compute_subnetwork.name` + * `google_compute_subnetwork.name` ## Supported Methods -- `GET`: Get a gcp-compute-subnetwork by its "name" -- `LIST`: List all gcp-compute-subnetwork -- ~~`SEARCH`~~ +* `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. +Every subnetwork is created inside exactly one VPC network. This link represents that parent–child relationship, allowing Overmind to show which VPC a particular subnetwork belongs to and, conversely, to enumerate all subnetworks within a given VPC. + +### [`gcp-compute-public-delegated-prefix`](/sources/gcp/Types/gcp-compute-public-delegated-prefix) + +A public delegated prefix can be assigned to a subnetwork so that resources inside the subnet can use public IPv4 addresses from that prefix. This link highlights which delegated prefixes are associated with, or routed through, the subnetwork, helping users trace external IP allocations and their exposure. \ No newline at end of file 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 index 980032c0..fcce0a73 100644 --- 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 @@ -3,15 +3,21 @@ 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 +A **GCP Compute Target HTTP Proxy** routes incoming HTTP requests to the appropriate backend service based on rules defined in a URL map. It terminates the client connection, consults the associated `google_compute_url_map`, and then forwards traffic to the selected backend (for example, a backend service or serverless NEG). Target HTTP proxies are a key component of Google Cloud external HTTP(S) Load Balancing. +See the official documentation for full details: https://cloud.google.com/load-balancing/docs/target-proxies#target_http_proxy **Terrafrom Mappings:** -- `google_compute_target_http_proxy.name` + * `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`~~ +* `GET`: Get a gcp-compute-target-http-proxy by its "name" +* `LIST`: List all gcp-compute-target-http-proxy +* ~~`SEARCH`~~ + +## Possible Links + +### [`gcp-compute-url-map`](/sources/gcp/Types/gcp-compute-url-map) + +A Target HTTP Proxy must reference exactly one URL map. Overmind uses this link to trace from the proxy to the URL map that defines its routing rules, enabling you to understand and surface any risks associated with misconfigured path matchers or backend services. \ No newline at end of file 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 index 1f9a673c..ea74733a 100644 --- 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 @@ -3,29 +3,29 @@ 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 +A **Target HTTPS Proxy** is a global Google Cloud resource that terminates incoming HTTPS connections at the edge of Google’s network, presents one or more SSL certificates, and then forwards the decrypted requests to the appropriate backend service according to a URL map. In essence, it is the control point that binds SSL certificates, SSL policies, and URL maps together to enable HTTPS traffic on an External HTTP(S) Load Balancer. +For full details see the official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/targetHttpsProxies **Terrafrom Mappings:** -- `google_compute_target_https_proxy.name` + * `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`~~ +* `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. +A Target HTTPS Proxy references one or more SSL certificates that it presents to clients during the TLS handshake. Overmind links these certificates so you can track which certificate is in use and assess expiry or misconfiguration risks. ### [`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. +An optional SSL policy can be attached to a Target HTTPS Proxy to enforce minimum TLS versions and cipher suites. Overmind exposes this link to highlight the security posture enforced on the proxy. ### [`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. +Every Target HTTPS Proxy must point to exactly one URL map, which defines how incoming requests are routed to backend services. Overmind links the URL map so you can trace the full request path and evaluate routing risks before deployment. \ No newline at end of file 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 index b7b49d1b..6cb8537d 100644 --- 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 @@ -3,28 +3,28 @@ 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 +A Google Cloud Compute Target Pool is a regional grouping of VM instances that acts as the backend for the legacy TCP/UDP network load balancer. The pool defines which instances receive traffic, the optional session-affinity policy, the associated health checks that determine instance health, and an optional fail-over target pool for backup. See the official documentation for full details: https://cloud.google.com/compute/docs/reference/rest/v1/targetPools **Terrafrom Mappings:** -- `google_compute_target_pool.id` + * `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). +* `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. +A target pool may reference one or more health checks through its `healthChecks` field. These health checks are used by Google Cloud to probe the instances in the pool and decide whether traffic should be sent to a particular VM. ### [`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. +Each target pool contains a list of VM instances (`instances` field) that will receive load-balanced traffic. Overmind links the pool to every instance it contains. ### [`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. +A target pool can specify another target pool as its `backupPool` to provide fail-over capacity, and it can itself be referenced as a backup by other pools. Overmind surfaces these peer-to-peer relationships between target pools. \ No newline at end of file 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 index 40025ed3..d893e7e5 100644 --- 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 @@ -3,21 +3,21 @@ 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 +A Google Cloud Platform (GCP) Compute URL Map is a routing table used by HTTP(S) load balancers to decide where an incoming request should be sent. It matches the request’s host name and URL path to a set of rules and then forwards the traffic to the appropriate backend service or backend bucket. URL Maps make it possible to implement advanced traffic-management patterns such as domain-based and path-based routing, default fall-back targets, and traffic migration between versions of a service. +Official documentation: https://cloud.google.com/load-balancing/docs/url-map-concepts **Terrafrom Mappings:** -- `google_compute_url_map.name` + * `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`~~ +* `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. +A URL Map points to one or more backend services as its routing targets. Each rule in the map specifies which `gcp-compute-backend-service` should receive the traffic that matches the rule’s host and path conditions. \ No newline at end of file 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 index b51f2540..7ed61fa9 100644 --- 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 @@ -3,21 +3,21 @@ 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 +A GCP Compute High-Availability (HA) VPN Gateway is a regional resource that provides secure, encrypted IPsec tunnels between a Google Cloud Virtual Private Cloud (VPC) network and peer networks (on-premises data centres, other clouds, or different GCP projects). The gateway offers redundancy by using two external interfaces, each of which can establish a pair of active tunnels, ensuring traffic continues to flow even during maintenance events or failures. Because the gateway is tightly coupled to a specific VPC network and region, it influences routing, firewall behaviour and overall network reachability. +See the official Google Cloud documentation for full details: https://cloud.google.com/network-connectivity/docs/vpn/concepts/overview **Terrafrom Mappings:** -- `google_compute_ha_vpn_gateway.name` + * `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`~~ +* `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. +Each HA VPN Gateway is created inside a single VPC network. Linking the gateway to its `gcp-compute-network` allows Overmind to trace which IP ranges, routes and firewall rules may be affected by the gateway’s tunnels, and to evaluate the blast radius of any proposed changes to either resource. \ No newline at end of file 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 index ddd9908b..ea77ac05 100644 --- 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 @@ -3,29 +3,29 @@ 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 +A Compute VPN Tunnel is the logical link that carries encrypted IP-sec traffic between Google Cloud and another network. It is created on top of a Google Cloud VPN Gateway and points at a peer gateway, defining parameters such as IKE version, shared secrets and traffic selectors. Each tunnel secures packets as they traverse the public Internet, allowing workloads in a VPC network to communicate privately with on-premises resources, other clouds, or additional GCP projects. +Official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/vpnTunnels **Terrafrom Mappings:** -- `google_compute_vpn_tunnel.name` + * `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`~~ +* `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. +A VPN tunnel targets an External VPN Gateway when its peer endpoint resides outside Google Cloud. The tunnel resource holds the reference that binds the Google side of the connection to the defined external gateway interface. ### [`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. +For dynamic (BGP) routing, a VPN tunnel is attached to a Cloud Router. The router exchanges routes with the peer across the tunnel, advertising VPC prefixes and learning remote prefixes. ### [`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. +Every VPN tunnel is created on a specific VPN Gateway (Classic or HA). The gateway provides the Google Cloud termination point, while the tunnel specifies the individual encrypted session parameters. \ No newline at end of file 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 index eed12aa9..bfbf35de 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-cluster.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-cluster.md @@ -3,45 +3,53 @@ 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 +Google Kubernetes Engine (GKE) Container Clusters provide fully-managed Kubernetes control planes running on Google Cloud. A cluster groups the Kubernetes control plane and the worker nodes that run your containerised workloads, and exposes a single API endpoint for deployment and management. Clusters can be regional or zonal, support autoscaling, automatic upgrades and many advanced networking, security and observability features. +Official documentation: https://cloud.google.com/kubernetes-engine/docs/concepts/kubernetes-engine-overview **Terrafrom Mappings:** -- `google_container_cluster.id` + * `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. +* `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-big-query-dataset`](/sources/gcp/Types/gcp-big-query-dataset) + +GKE can export usage metering and cost allocation data, as well as logs via Cloud Logging sinks, to a BigQuery dataset. When a cluster is configured for resource usage metering, it is linked to the destination dataset. + ### [`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. +Clusters may use a customer-managed encryption key (CMEK) from Cloud KMS to encrypt Kubernetes Secrets and other etcd data at rest. The CMEK key configured for a cluster or for its persistent disks is therefore related. + +### [`gcp-cloud-kms-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) + +A specific key version is referenced by the cluster for CMEK encryption. Rotating the key version affects the cluster’s data-at-rest encryption. ### [`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. +Every cluster is deployed into a VPC network; all control-plane and node traffic flows across this network. The network selected during cluster creation is linked here. ### [`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. +If the cluster uses sole-tenant nodes or node auto-provisioning, the underlying Compute Engine Node Groups that host GKE nodes are related to the cluster. ### [`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. +Clusters (and their node pools) are placed in one or more subnets within the VPC for pod and service IP ranges. These subnetworks are therefore 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. +A cluster contains one or more node pools that define the configuration of its worker nodes (machine type, autoscaling settings, etc.). Each node pool resource is directly associated with its parent cluster. ### [`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. +GKE uses IAM service accounts for the control plane, node VMs and workload identity. Service accounts granted to the cluster (e.g., Google APIs service agent, node service account) are linked. ### [`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. +Cluster audit logs, events or notifications can be exported to a Pub/Sub topic (e.g., via Log Sinks or Notification Channels). Any topic configured as a destination for the cluster is related here. \ No newline at end of file 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 index dff4c40d..58eeb4a0 100644 --- 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 @@ -3,33 +3,46 @@ 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. +Google Kubernetes Engine (GKE) runs worker nodes in groups called *node pools*. +Each pool defines the machine type, disk configuration, Kubernetes version and other attributes for the virtual machines that will back your workloads, and can be scaled or upgraded independently from the rest of the cluster. Official documentation: https://cloud.google.com/kubernetes-engine/docs/concepts/node-pools **Terrafrom Mappings:** -- `google_container_node_pool.id` + * `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]" +* `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. +When customer-managed encryption keys (CMEK) are enabled for node disks, the node pool stores a reference to the Cloud KMS crypto key that encrypts each node’s boot and attached data volumes. + +### [`gcp-compute-instance-group-manager`](/sources/gcp/Types/gcp-compute-instance-group-manager) + +Every node pool is implemented as a regional or zonal Managed Instance Group (MIG) that GKE creates and controls; the Instance Group Manager handles the lifecycle of the virtual machines that make up the pool. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Nodes launched by the pool are attached to a specific VPC network (and its associated routes and firewall rules), so the pool maintains a link to the Compute Network used by the cluster. ### [`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. +If the node pool is configured to run on sole-tenant nodes, it will reference the Compute Node Group that represents the underlying dedicated hosts reserved for those nodes. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +The pool records the particular subnetwork into which its nodes are placed, controlling the IP range from which node addresses are allocated. ### [`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. +A node pool is a child resource of a GKE cluster; this link identifies the parent `gcp-container-cluster` that owns and orchestrates the pool. ### [`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. +Each node runs with a Google service account that provides credentials for pulling container images, writing logs, and calling Google APIs. The pool stores a reference to that IAM Service Account. \ No newline at end of file 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 index dccaedcb..6de737ca 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataform-repository.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataform-repository.md @@ -3,29 +3,33 @@ 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 +A Google Cloud Dataform Repository represents the source-controlled codebase that defines your Dataform workflows. It stores SQLX files, declarations and configuration that Dataform uses to build, test and deploy transformations in BigQuery. A repository can point to an internal workspace or to an external Git repository and may reference service accounts, Secret Manager secrets and customer-managed encryption keys. +Official documentation: https://cloud.google.com/dataform/reference/rest **Terrafrom Mappings:** -- `google_dataform_repository.id` + * `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. +* `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. +A repository can be configured with a customer-managed encryption key (`kms_key_name`) to encrypt its metadata and compiled artefacts, creating a dependency on the corresponding Cloud KMS crypto-key. + +### [`gcp-cloud-kms-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) + +If CMEK is enabled, the repository points to a specific crypto-key version that is actually used for encryption; rotating or disabling that version will affect the repository. ### [`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. +Dataform uses a service account to fetch code from remote Git repositories and to execute compilation and workflow tasks; the repository stores the e-mail address of that service account. ### [`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. +When a repository is linked to an external Git provider, the authentication token is stored in Secret Manager. The field `authentication_token_secret_version` references the secret (and version) that holds the token. \ No newline at end of file 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 index af6be780..9fe13d0b 100644 --- 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 @@ -3,15 +3,15 @@ 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 +A Dataplex Aspect Type is a top-level resource within Google Cloud Dataplex’s metadata service that defines the structure of a metadata “aspect” – a reusable schema describing a set of attributes you want to attach to data assets (for example, data quality scores or business classifications). Once an aspect type is created, individual assets such as tables, files or columns can be annotated with concrete “aspects” that conform to that schema, ensuring consistent, centrally-governed metadata across your lake. +For further details see the official API reference: https://cloud.google.com/dataplex/docs/reference/rest/v1/projects.locations.aspectTypes **Terrafrom Mappings:** -- `google_dataplex_aspect_type.id` + * `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. +* `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. \ No newline at end of file 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 index a9caa00a..4cd51511 100644 --- 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 @@ -3,20 +3,25 @@ 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 +A GCP Dataplex Data Scan is a first-class Dataplex resource that encapsulates the configuration and schedule for profiling data or validating data-quality rules against a registered asset such as a BigQuery table or files held in Cloud Storage. Each scan lives in a specific Google Cloud location and records its execution history, metrics and detected issues, allowing teams to understand data health before downstream workloads rely on it. +For full details see the official REST reference: https://cloud.google.com/dataplex/docs/reference/rest/v1/projects.locations.dataScans **Terrafrom Mappings:** -- `google_dataplex_datascan.id` + * `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. +* `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-big-query-table`](/sources/gcp/Types/gcp-big-query-table) + +A Dataplex Data Scan may target a BigQuery table as its data source; linking the scan to the table lets Overmind trace quality findings back to the exact table that will be affected by the deployment. + ### [`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. +When the data asset under review is a set of files stored in Cloud Storage, Dataplex references the underlying bucket. Linking the scan to the bucket reveals how changes to bucket configuration or contents could influence upcoming scan results. \ No newline at end of file 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 index 04d5e81f..8a40aa47 100644 --- 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 @@ -3,15 +3,15 @@ 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 +A Dataplex Entry Group is a logical container that holds one or more metadata entries within Google Cloud’s unified Data Catalog. By grouping related entries together, it helps data stewards organise, secure and search metadata that describe the underlying data assets managed by Dataplex (such as tables, files or streams). Each Entry Group lives in a specific project and location and can be granted IAM permissions independently, allowing fine-grained access control over the metadata it contains. +Official documentation: https://cloud.google.com/data-catalog/docs/reference/rest/v1/projects.locations.entryGroups **Terrafrom Mappings:** -- `google_dataplex_entry_group.id` + * `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. +* `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. \ No newline at end of file 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 index 9532af2f..1719da66 100644 --- 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 @@ -3,14 +3,15 @@ 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. +A GCP Dataproc Autoscaling Policy defines the rules that Google Cloud Dataproc uses to automatically add or remove worker nodes from a Dataproc cluster in response to workload demand. By specifying target utilisation levels, cooldown periods, graceful decommissioning time-outs and per-node billing settings, the policy ensures that clusters expand to meet spikes in processing requirements and shrink when demand falls, optimising both performance and cost. +For a full description of each field and the underlying API, see the official Google Cloud documentation: https://cloud.google.com/dataproc/docs/reference/rest/v1/projects.regions.autoscalingPolicies. -**Terrafrom Mappings:** +**Terraform Mappings:** -- `google_dataproc_autoscaling_policy.name` + * `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`~~ +* `GET`: Get a gcp-dataproc-autoscaling-policy by its "name" +* `LIST`: List all gcp-dataproc-autoscaling-policy +* ~~`SEARCH`~~ \ No newline at end of file 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 index 23805167..3de7966b 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-cluster.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-cluster.md @@ -3,52 +3,64 @@ 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. +A Google Cloud Dataproc Cluster is a managed group of Compute Engine virtual machines configured to run big-data workloads such as Apache Hadoop, Spark, Hive and Presto. Dataproc abstracts away the operational overhead of provisioning, configuring and scaling the underlying infrastructure, allowing you to launch fully-featured clusters in minutes and shut them down just as quickly. See the official documentation for full details: https://cloud.google.com/dataproc/docs/concepts/overview **Terrafrom Mappings:** -- `google_dataproc_cluster.name` + * `google_dataproc_cluster.name` ## Supported Methods -- `GET`: Get a gcp-dataproc-cluster by its "name" -- `LIST`: List all gcp-dataproc-cluster -- ~~`SEARCH`~~ +* `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. +If customer-managed encryption keys (CMEK) are enabled, a Dataproc Cluster references a Cloud KMS Crypto Key to encrypt the persistent disks attached to its virtual machines. ### [`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. +Each node in a Dataproc Cluster boots from a specific Compute Engine image (e.g., a Dataproc-prebuilt image or a custom image), so the cluster has a dependency on that image. ### [`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. +Dataproc automatically creates Managed Instance Groups (MIGs) for the primary, worker and optional secondary-worker node pools; these MIGs are children of the Dataproc Cluster. ### [`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. +The cluster’s VMs are attached to a particular VPC network, dictating their reachability, firewall rules and routing behaviour. ### [`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. +If the cluster is deployed on sole-tenant nodes, it is associated with a Compute Node Group that provides dedicated hardware isolation. ### [`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. +Within the selected VPC, the Dataproc Cluster attaches its instances to a specific subnetwork where IP addressing, Private Google Access and regional placement are defined. + +### [`gcp-container-cluster`](/sources/gcp/Types/gcp-container-cluster) + +For Dataproc on GKE deployments, the Dataproc Cluster is layered on top of an existing Google Kubernetes Engine cluster, creating a parent–child relationship. + +### [`gcp-container-node-pool`](/sources/gcp/Types/gcp-container-node-pool) + +When running Dataproc on GKE, the workloads execute on one or more GKE node pools; the Dataproc service references these node pools for capacity. ### [`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. +A Dataproc Cluster can be bound to an Autoscaling Policy that dynamically adjusts the number of worker nodes based on workload metrics. + +### [`gcp-dataproc-cluster`](/sources/gcp/Types/gcp-dataproc-cluster) + +Clusters can reference other clusters as templates or in workflows that orchestrate multiple clusters; Overmind represents these peer or predecessor relationships with a self-link. ### [`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. +The VMs within the cluster run under one or more IAM Service Accounts that grant them permissions to access other Google Cloud services. ### [`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. +During creation, the cluster specifies Cloud Storage buckets for staging, temp and log output, making those buckets upstream dependencies. \ No newline at end of file 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 index 7f296db9..d574e644 100644 --- 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 @@ -3,25 +3,25 @@ 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 +A Google Cloud DNS Managed Zone is a logical container for DNS resource records that share the same DNS name suffix. Managed zones can be configured as public (resolvable from the internet) or private (resolvable only from one or more selected VPC networks). They are the fundamental unit that Cloud DNS uses to host, serve and manage authoritative DNS data for your domains. +Official documentation: https://cloud.google.com/dns/docs/zones **Terrafrom Mappings:** -- `google_dns_managed_zone.name` + * `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`~~ +* `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. +Private managed zones are explicitly linked to one or more VPC networks. The association determines which networks can resolve the zone’s records, so an Overmind relationship helps surface reachability and leakage risks between a DNS zone and the networks that consume it. ### [`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. +GKE clusters frequently create or rely on Cloud DNS managed zones for service discovery and in-cluster load-balancing (e.g., when CloudDNS for Service Directory is enabled). Mapping a cluster to its managed zones reveals dependencies that affect name resolution, cross-cluster communication and potential namespace conflicts. \ No newline at end of file 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 index d3797d1d..e8af18e7 100644 --- 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 @@ -3,15 +3,15 @@ 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 +A **Google Cloud Essential Contact** represents an email address or Google Group that Google Cloud will use to send important notifications about incidents, security issues, and other critical updates for a project, folder, or organisation. Each contact is stored under a parent resource (e.g. `projects/123456789`, `folders/987654321`, or `organizations/555555555`) and can be categorised by notification types such as `SECURITY`, `TECHNICAL`, or `LEGAL`. +For further details, refer to the official Google Cloud documentation: https://cloud.google.com/resource-manager/docs/reference/essentialcontacts/rest **Terrafrom Mappings:** -- `google_essential_contacts_contact.id` + * `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]". +* `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]". \ No newline at end of file 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 index f9a224ba..48e4de96 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-file-instance.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-file-instance.md @@ -3,25 +3,25 @@ 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. +A GCP File Instance represents a Cloud Filestore instance – a managed network file storage appliance that provides an NFSv3 or NFSv4-compatible file share, typically used by GKE clusters or Compute Engine VMs that require shared, POSIX-compliant storage. Each instance is created in a specific GCP region and zone, connected to a VPC network, and exposes one or more file shares (called “filesets”) over a private RFC-1918 address. Instances can be customised for capacity and performance tiers, and may optionally use customer-managed encryption keys (CMEK) for data-at-rest encryption. +Official documentation: https://cloud.google.com/filestore/docs/overview **Terrafrom Mappings:** -- `google_filestore_instance.id` + * `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. +* `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. +A Filestore instance can be encrypted with a customer-managed Cloud KMS key (CMEK). The link shows which KMS Crypto Key is protecting the data-at-rest of this storage appliance. ### [`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. +Filestore instances are deployed into and reachable through a specific VPC network. This link identifies the Compute Network whose subnet provides the private IP addresses through which clients access the file share. \ No newline at end of file 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 index 0867be52..e5f57580 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-role.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-role.md @@ -3,11 +3,10 @@ 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 +A **Google Cloud IAM Role** is a logical grouping of one or more IAM permissions that can be granted to principals (users, service accounts, groups or Google Workspace domains) to control their access to Google Cloud resources. Roles come in three flavours—basic, predefined and custom—allowing organisations to strike a balance between least-privilege access and administrative convenience. For a full explanation see the 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`~~ +* `GET`: Get a gcp-iam-role by its "name" +* `LIST`: List all gcp-iam-role +* ~~`SEARCH`~~ \ No newline at end of file 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 index 79313f2f..9adfdb13 100644 --- 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 @@ -3,21 +3,21 @@ 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 +A GCP IAM Service Account Key is a cryptographic key-pair (private and public) that is bound to a specific IAM service account. Possessing the private half of the key allows a workload or user to authenticate to Google Cloud APIs as that service account, making the key one of the most sensitive objects in any Google Cloud environment. Keys can be user-managed or Google-managed, rotated, disabled or deleted, and each service account can hold up to ten user-managed keys at a time. Mis-management of these keys can lead to credential leakage and unauthorised access. +Official documentation: https://cloud.google.com/iam/docs/creating-managing-service-account-keys **Terrafrom Mappings:** -- `google_service_account_key.id` + * `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" +* `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. +A Service Account Key is always subordinate to, and uniquely associated with, a single IAM service account. Overmind links the key back to its parent service account so you can trace which workload the key belongs to, understand the permissions it inherits, and assess the blast radius should the key be compromised. \ No newline at end of file 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 index 92c34a8d..f5868dc0 100644 --- 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 @@ -3,25 +3,26 @@ 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). +A GCP IAM Service Account is a special kind of Google identity that an application or VM instance uses to make authorised calls to Google Cloud APIs, rather than an end-user. Each service account is identified by an email‐style string (e.g. `my-sa@project-id.iam.gserviceaccount.com`) and a stable numeric `unique_id`. Service accounts can be granted IAM roles, can own resources, and may have one or more cryptographic keys used for authentication. +For full details see the official documentation: https://cloud.google.com/iam/docs/service-accounts **Terrafrom Mappings:** -- `google_service_account.email` -- `google_service_account.unique_id` + * `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`~~ +* `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. +Every service account is created inside a single Cloud Resource Manager project. This link lets you navigate from the service account to the project that owns it, revealing project-level policies and context. ### [`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. +Service account keys are cryptographic credentials associated with a service account. This link lists all keys (active, disabled or expired) that belong to the current service account, allowing you to audit key rotation and exposure risks. \ No newline at end of file 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 index 9c101db9..fb4fc7b6 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-bucket.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-bucket.md @@ -3,21 +3,26 @@ 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 +A GCP Logging Bucket is a regional or multi-regional storage container managed by Cloud Logging that stores log entries routed from one or more Google Cloud projects, folders or organisations. Buckets provide fine-grained control over where logs are kept, how long they are retained, and which encryption keys protect them. Log buckets behave similarly to Cloud Storage buckets, but are optimised for log data and are accessed through the Cloud Logging API rather than through Cloud Storage. +See the official documentation for full details: https://cloud.google.com/logging/docs/storage + ## Supported Methods -- `GET`: Get a gcp-logging-bucket by its "locations|buckets" -- ~~`LIST`~~ -- `SEARCH`: Search for gcp-logging-bucket by its "locations" +* `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. +A logging bucket can be configured to use customer-managed encryption keys (CMEK). When CMEK is enabled, the bucket references a Cloud KMS Crypto Key that holds the symmetric key material used to encrypt and decrypt the stored log entries. + +### [`gcp-cloud-kms-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) + +If CMEK is active, the bucket also keeps track of the specific key version that is currently in use. This link represents the exact Crypto Key Version providing encryption for the bucket at a given point in time. ### [`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. +Cloud Logging uses service accounts to write, read or route logs into a bucket. The bucket’s IAM policy may grant `roles/logging.bucketWriter` or `roles/logging.viewer` to particular service accounts, and the Log Router’s reserved service account must have permission to encrypt data when CMEK is enabled. \ No newline at end of file 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 index e5e10117..d1638e90 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-link.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-link.md @@ -3,24 +3,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. +A GCP Logging Link is a Cloud Logging resource that continuously streams the log entries stored in a specific Log Bucket into an external BigQuery dataset. By configuring a link you enable near-real-time analytics of your logs with BigQuery without the need for manual exports or scheduled jobs. Links are created under the path + +`projects|folders|organizations|billingAccounts / locations / buckets / links` + +and each link specifies the destination BigQuery dataset, IAM writer identity, and lifecycle state. +For further details see Google’s official documentation: https://cloud.google.com/logging/docs/reference/v2/rest/v2/projects.locations.buckets.links -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" +* `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. +A logging link targets exactly one BigQuery dataset; Overmind establishes this edge so you can trace which dataset is receiving log entries from the 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. +The logging link is defined inside a specific Log Bucket; this relationship lets you see which buckets are sending their logs onwards and to which destinations. \ No newline at end of file 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 index 0502885d..9805d0a0 100644 --- 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 @@ -3,11 +3,12 @@ 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 +A GCP Logging Saved Query is a reusable, named log query that is stored in Google Cloud Logging’s Logs Explorer. It contains the filter expression (or Log Query Language statement), any configured time-range presets and display options, allowing teams to quickly rerun common searches, share queries across projects, and use them as the basis for dashboards, log-based metrics or alerting policies. Because Saved Queries are resources in their own right, they can be created, read, updated and deleted through the Cloud Logging API, and are uniquely identified by the combination of the Google Cloud location and the query name. +Official documentation: https://cloud.google.com/logging/docs/view/building-queries + ## Supported Methods -- `GET`: Get a gcp-logging-saved-query by its "locations|savedQueries" -- ~~`LIST`~~ -- `SEARCH`: Search for gcp-logging-saved-query by its "locations" +* `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 index b1e20cc0..b0fe6cad 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-sink.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-sink.md @@ -3,29 +3,33 @@ 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 +A Logging Sink in Google Cloud Platform (GCP) is a routing rule that selects log entries with a user-defined filter and exports them to a chosen destination such as BigQuery, Cloud Storage, Pub/Sub, or another Cloud Logging bucket. Sinks are the building blocks of GCP’s Log Router and are used to retain, analyse or stream logs outside of the originating project, folder or organisation. +Official documentation: https://cloud.google.com/logging/docs/export ## Supported Methods -- `GET`: Get GCP Logging Sink by "gcp-logging-sink-name" -- `LIST`: List all GCP Logging Sink items -- ~~`SEARCH`~~ +* `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. +If the sink’s destination is a BigQuery table, it must reference a BigQuery dataset where the tables will be created and written to. The dataset therefore appears as a child dependency of the logging sink. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Every sink is assigned a writer_identity, which is an IAM service account that needs permission to write into the chosen destination. The sink’s correct operation depends on this service account having the required roles on the target resource. ### [`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. +A sink can route logs to another Cloud Logging bucket (including aggregated buckets at the folder or organisation level). In this case the sink targets, and must have write access to, the specified logging bucket. ### [`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. +When the destination is Pub/Sub, the sink exports each matching log entry as a message on a particular topic. The topic therefore represents an external linkage for onward streaming or event-driven processing. ### [`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. +For archival purposes a sink may export logs to a Cloud Storage bucket. The bucket must exist and grant the sink’s writer service account permission to create objects, making the storage bucket a direct dependency of the sink. \ No newline at end of file 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 index 3521f688..22acddea 100644 --- 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 @@ -3,21 +3,20 @@ 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 +A Google Cloud Monitoring Alert Policy is a configuration object that defines the conditions under which Cloud Monitoring should create an incident, how incidents are grouped, and which notification channels should be used to inform operators. Alert policies enable proactive observation of metrics, logs and uptime checks across Google Cloud services so that you can respond quickly to anomalies. For more detail see the official Google Cloud documentation: [Create and manage alerting policies](https://cloud.google.com/monitoring/alerts). **Terrafrom Mappings:** -- `google_monitoring_alert_policy.id` + * `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). +* `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. +An alert policy can reference one or more notification channels so that, when its conditions are met, Cloud Monitoring sends notifications (e-mails, webhooks, SMS, etc.) through the linked gcp-monitoring-notification-channels. \ No newline at end of file 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 index 2e1aaa0f..65986d2b 100644 --- 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 @@ -3,15 +3,15 @@ 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 +A GCP Monitoring Custom Dashboard is a user-defined collection of charts and widgets that presents metrics, logs, and alerts for resources running in Google Cloud or on-premises. It allows platform teams to visualise performance, capacity, and health in a single view that can be shared across projects. Custom dashboards are managed through Cloud Monitoring and can be created or modified via the Google Cloud Console, the Cloud Monitoring API, or infrastructure-as-code tools such as Terraform. +For full details, see the official documentation: https://cloud.google.com/monitoring/charts/dashboards **Terrafrom Mappings:** -- `google_monitoring_dashboard.id` + * `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. +* `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. \ No newline at end of file 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 index f29eba8b..8b181969 100644 --- 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 @@ -3,15 +3,20 @@ 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. +A **Google Cloud Monitoring Notification Channel** specifies where and how Cloud Monitoring delivers alert notifications—for example via email, SMS, Cloud Pub/Sub, Slack or PagerDuty. Each channel stores the configuration necessary for a particular medium (address, webhook URL, Pub/Sub topic name, etc.) and can be referenced by one or more alerting policies. For full details, see the official Google documentation: https://cloud.google.com/monitoring/support/notification-options **Terrafrom Mappings:** -- `google_monitoring_notification_channel.name` + * `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). +* `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). + +## Possible Links + +### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) + +If the notification channel’s `type` is `pubsub`, the channel references a specific Cloud Pub/Sub topic where alert messages are published. Overmind therefore links the notification channel to the corresponding `gcp-pub-sub-topic` resource so that you can trace how alerts propagate into event-driven workflows or downstream systems. \ No newline at end of file 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 index c9c6788c..109bff0d 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-orgpolicy-policy.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-orgpolicy-policy.md @@ -3,15 +3,21 @@ 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 +An Organisation Policy (orgpolicy) in Google Cloud is a resource that applies a constraint to part of the resource hierarchy (organisation, folder, or project). It allows administrators to enforce governance rules—such as restricting the regions in which resources may be created, blocking the use of certain services, or mandating specific network configurations—before workloads are deployed. +For full details see Google’s official documentation: https://cloud.google.com/resource-manager/docs/organization-policy/overview **Terrafrom Mappings:** -- `google_org_policy_policy.name` + * `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). +* `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). + +## Possible Links + +### [`gcp-cloud-resource-manager-project`](/sources/gcp/Types/gcp-cloud-resource-manager-project) + +A project is one of the resource hierarchy levels to which an Organisation Policy can be attached. Each gcp-orgpolicy-policy documented here is therefore linked to the gcp-cloud-resource-manager-project that the policy governs. \ No newline at end of file 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 index 16c2256e..cbd41890 100644 --- 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 @@ -3,33 +3,39 @@ 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 +A Google Cloud Pub/Sub subscription represents a stream of messages delivered from a single Pub/Sub topic to a consumer application. Each subscription defines how, where and for how long messages are retained, whether the delivery is push or pull, any filters or dead-letter policies, and the IAM principals that are allowed to read from it. Official documentation can be found at Google Cloud – Pub/Sub Subscriptions: https://cloud.google.com/pubsub/docs/subscription-overview **Terrafrom Mappings:** -- `google_pubsub_subscription.name` + * `google_pubsub_subscription.name` + * `google_pubsub_subscription_iam_binding.subscription` + * `google_pubsub_subscription_iam_member.subscription` + * `google_pubsub_subscription_iam_policy.subscription` ## Supported Methods -- `GET`: Get a gcp-pub-sub-subscription by its "name" -- `LIST`: List all gcp-pub-sub-subscription -- ~~`SEARCH`~~ +* `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. +Pub/Sub can deliver messages directly into BigQuery by means of a BigQuery subscription. When such an integration is configured, the subscription is linked to the destination BigQuery table. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Service accounts are granted roles such as `roles/pubsub.subscriber` on the subscription so that applications can pull or acknowledge messages, or so that Pub/Sub can impersonate them for push deliveries. These IAM bindings create a relationship between the subscription and the service accounts. ### [`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. +Multiple subscriptions can point at the same topic, or one subscription may forward undelivered messages to another subscription via a dead-letter topic. Overmind shows these peer or chained subscriptions as related items. ### [`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`. +Every subscription is attached to exactly one topic. All messages published to that topic are made available to the subscription, making the topic the primary upstream dependency. ### [`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. +Cloud Storage buckets can emit object-change notifications to a Pub/Sub topic. If the subscription listens to such a topic, it is indirectly linked to the bucket that generated the events, allowing you to trace the flow from storage changes to message consumption. \ No newline at end of file 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 index 92968f0f..a9762627 100644 --- 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 @@ -3,24 +3,32 @@ 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. +A **Cloud Pub/Sub Topic** is a named message channel in Google Cloud Platform that receives messages from publishers and delivers them to subscribers. Topics decouple senders and receivers, allowing highly-scalable, asynchronous communication between services. Every message published to a topic is retained for the duration of its acknowledgement window and can be encrypted with a customer-managed key. +For comprehensive information, see the official documentation: https://cloud.google.com/pubsub/docs/create-topic. **Terrafrom Mappings:** -- `google_pubsub_topic.name` + * `google_pubsub_topic.name` + * `google_pubsub_topic_iam_binding.topic` + * `google_pubsub_topic_iam_member.topic` + * `google_pubsub_topic_iam_policy.topic` ## Supported Methods -- `GET`: Get a gcp-pub-sub-topic by its "name" -- `LIST`: List all gcp-pub-sub-topic -- ~~`SEARCH`~~ +* `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. +A Pub/Sub topic may be encrypted using a customer-managed encryption key (CMEK). When CMEK is enabled, the topic resource holds a reference to the Cloud KMS Crypto Key that protects message data at rest. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Access to publish or subscribe is controlled through IAM roles that are granted to service accounts on the topic. The topic’s IAM policy therefore links it to any service account that has roles such as `roles/pubsub.publisher` or `roles/pubsub.subscriber` on the resource. ### [`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. +Cloud Storage buckets can be configured to send change notifications to a Pub/Sub topic (for example, object create or delete events). In such configurations, the bucket acts as a publisher, and the topic appears as a dependent destination for bucket event notifications. \ No newline at end of file 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 index fe0ec384..38c08485 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-redis-instance.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-redis-instance.md @@ -3,29 +3,28 @@ 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 +A GCP Redis Instance is a fully managed, in-memory data store provided by Cloud Memorystore for Redis. It offers a drop-in, highly available Redis service that handles provisioning, patching, scaling, monitoring and automatic fail-over, allowing you to use Redis as a cache or primary database without managing the underlying infrastructure yourself. See the official documentation for details: https://cloud.google.com/memorystore/docs/redis **Terrafrom Mappings:** -- `google_redis_instance.id` + * `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. +* `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. +If Customer-Managed Encryption Keys (CMEK) are enabled for the Redis instance, the data at rest is encrypted with a Cloud KMS Crypto Key. The Redis instance therefore depends on — and is cryptographically linked to — the specified `gcp-cloud-kms-crypto-key`. ### [`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. +A Redis instance is deployed inside a specific VPC network and is reachable only via an internal IP address in that network. Consequently, each instance is associated with a `gcp-compute-network`, which determines its connectivity and firewall boundaries. ### [`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. +When TLS is enabled for a Redis instance, it can reference a Compute Engine SSL certificate resource to present during encrypted client connections. The `gcp-compute-ssl-certificate` therefore represents the server certificate used to secure traffic to the Redis instance. \ No newline at end of file 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 index 5368eede..0fef4fe1 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-revision.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-revision.md @@ -3,44 +3,50 @@ 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 +A Cloud Run **Revision** is an immutable snapshot of a Cloud Run Service configuration at a particular point in time. Each time you deploy new code or change configuration, Cloud Run automatically creates a new revision and routes traffic according to your settings. A revision defines the container image to run, environment variables, resource limits, networking options, service account, secret mounts and more. Once created, a revision can never be modified – you can only create a new one. +Official documentation: https://cloud.google.com/run/docs/reference/rest/v1/namespaces.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" +* `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. +The container image specified in the revision is often stored in Artifact Registry. The revision therefore has a **uses-image** relationship with the referenced Docker image. ### [`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. +If the revision is configured with a customer-managed encryption key (CMEK) for encrypted secrets or volumes, it will reference the corresponding Cloud KMS Crypto Key. ### [`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. +When a revision is set up to use Serverless VPC Access, it connects to a specific VPC network, creating a **connects-to-network** relationship. ### [`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. +The Serverless VPC Access connector used by the revision is attached to a particular subnetwork, so the revision is indirectly linked to that subnetwork. ### [`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. +Each revision runs with an IAM service account whose permissions govern outbound calls and resource access. The revision therefore **runs-as** the referenced service account. ### [`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. +The revision is a child resource of a Cloud Run Service. All traffic routing and lifecycle events are managed at the service level. + +### [`gcp-secret-manager-secret`](/sources/gcp/Types/gcp-secret-manager-secret) + +Environment variables or mounted volumes in the revision can pull values from Secret Manager. This establishes a **consumes-secret** relationship. ### [`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. +If the revision defines Cloud SQL connections, it will list one or more Cloud SQL instances it can connect to through the Cloud SQL proxy. ### [`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. +A revision may read from or write to Cloud Storage buckets (for example for static assets or generated files) when granted the appropriate IAM permissions, creating a potential dependency on those buckets. \ No newline at end of file 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 index deff181d..501c218b 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-service.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-service.md @@ -3,52 +3,53 @@ 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 +Google Cloud Run Service is a fully-managed compute platform that automatically scales stateless containers on demand. A Service represents the user-facing abstraction of your application, managing one or more immutable Revisions of a container image and routing traffic to them. It provides configuration for networking, environment variables, secrets, concurrency, autoscaling and identity. +Official documentation: https://cloud.google.com/run/docs **Terrafrom Mappings:** -- `google_cloud_run_v2_service.id` + * `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" +* `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. +A Cloud Run Service deploys one specific container image; most commonly this image is stored in Artifact Registry. The link shows which image version the Service’s active Revision is based on. ### [`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`. +If the Service uses customer-managed encryption keys (CMEK) for at-rest encryption of logs, volumes or secrets, it will reference a 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`. +When the Service is configured with a VPC connector for egress or to reach private resources, it ultimately attaches to a specific 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. +The VPC connector also targets a concrete Subnetwork; this link identifies the precise subnet through which the Service’s traffic is routed. ### [`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`. +A Cloud Run Service runs with a dedicated runtime identity. This Service Account is used for accessing other Google Cloud resources and defines the permissions available to the container. ### [`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. +Each update to configuration or container image creates a new Revision. The Service points traffic to one or more of these Revisions; the link maps the parent-child relationship. ### [`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. +Environment variables or mounted volumes in the Service can be sourced from Secret Manager. Linked secrets indicate which sensitive values are injected at runtime. ### [`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`. +If Cloud SQL connections are configured via the Cloud SQL Auth Proxy side-car or built-in integration, the Service will reference one or more Cloud SQL instances. ### [`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`. +The Service may access files in Cloud Storage for static assets or as mounted volumes (Cloud Run volumes). Buckets listed here are those explicitly referenced by environment variables, IAM permissions or volume mounts. \ No newline at end of file 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 index be765812..13c17791 100644 --- 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 @@ -3,24 +3,25 @@ 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 +A Secret in Google Cloud Secret Manager is a secure, version-controlled container for sensitive data such as passwords, API keys, certificates, or any arbitrary text or binary payload. Each Secret holds one or more Secret Versions, allowing you to rotate or roll back the underlying data without changing the resource identifier that your applications refer to. Secrets are encrypted at rest with Google-managed keys by default, or you can supply a customer-managed Cloud KMS key. You can also configure Pub/Sub notifications to be emitted whenever a new version is added or other lifecycle events occur. +For full details see the official documentation: https://cloud.google.com/secret-manager/docs **Terrafrom Mappings:** -- `google_secret_manager_secret.secret_id` + * `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`~~ +* `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. +If a Secret is configured to use customer-managed encryption (CMEK), it references a Cloud KMS Crypto Key that performs the envelope encryption of all Secret Versions. Compromise or mis-configuration of the referenced KMS key directly affects the confidentiality and availability of the Secret’s payloads. ### [`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. +Secret Manager can publish events—such as the creation of a new Secret Version—to a Pub/Sub topic. This enables automated workflows like triggering Cloud Functions for secret rotation or auditing. The Secret therefore holds an optional link to any Pub/Sub topic configured for such notifications. \ No newline at end of file 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 index 32c701bf..55802996 100644 --- 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 @@ -3,11 +3,19 @@ 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 +The **Security Center Service** resource represents the configuration of Security Command Center (SCC) for a particular Google Cloud location. +Each instance of this resource indicates that SCC is running in the specified region and records the service‐wide settings that govern how findings are ingested, stored and surfaced. +Official documentation: https://cloud.google.com/security-command-center/docs/reference/security-center-management/rest/v1/projects.locations.securityCenterServices/list + ## 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". +* `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". + +## Possible Links + +### [`gcp-cloud-resource-manager-project`](/sources/gcp/Types/gcp-cloud-resource-manager-project) + +A Security Center Service exists **inside** a specific Google Cloud project – the project determines billing, IAM policies and the scope of resources that SCC monitors. The Overmind link lets you pivot from the project to every Security Center Service it has enabled (and vice-versa), helping you see which projects have security monitoring active in each region. \ No newline at end of file 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 index bb6b4f89..15d5ef4d 100644 --- 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 @@ -3,21 +3,21 @@ 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. +A **Service Directory Endpoint** represents a concrete network endpoint (host/IP address and port) that implements a Service Directory service within a namespace and location. Clients resolve a service and obtain one or more endpoints in order to make network calls. Endpoints can carry arbitrary key-value metadata and may point at instances running inside a VPC, on-premises, or in another cloud. Official documentation: https://cloud.google.com/service-directory/docs/reference/rest/v1/projects.locations.namespaces.services.endpoints **Terrafrom Mappings:** -- `google_service_directory_endpoint.id` + * `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. +* `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. +A Service Directory endpoint’s address usually resides within a VPC network. Linking an endpoint to its `gcp-compute-network` resource lets you trace which network the IP belongs to, ensuring that connectivity policies (firewalls, routes, private service access, etc.) permit clients to reach the service before deployment. \ No newline at end of file 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 index eb94c112..c2830fbf 100644 --- 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 @@ -3,18 +3,22 @@ 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 +A **Service Usage Service** represents an individual Google-managed API or service (e.g. `compute.googleapis.com`, `pubsub.googleapis.com`) and its enablement state inside a single GCP project. By querying this resource you can determine whether a particular service is currently enabled, disabled, or in another transitional state for that project, which is critical for understanding if downstream resources can be created successfully. +Official documentation: https://cloud.google.com/service-usage/docs/reference/rest/v1/services + ## Supported Methods -- `GET`: Get a gcp-service-usage-service by its "name" -- `LIST`: List all gcp-service-usage-service -- ~~`SEARCH`~~ +* `GET`: Get a gcp-service-usage-service by its "name" +* `LIST`: List all gcp-service-usage-service +* ~~`SEARCH`~~ ## Possible Links +### [`gcp-cloud-resource-manager-project`](/sources/gcp/Types/gcp-cloud-resource-manager-project) + +Every Service Usage Service exists **within** a single Cloud Resource Manager project. The project acts as the parent container and dictates billing, IAM policies and quota that apply to the service. + ### [`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. +A Pub/Sub topic can only be created or used if the **`pubsub.googleapis.com`** Service Usage Service is enabled in the same project. Overmind links the topic back to its enabling service so you can quickly spot configuration drift or missing API enablement that would prevent deployment. \ No newline at end of file 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 index 7206a904..ba49a385 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-database.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-database.md @@ -3,26 +3,32 @@ 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 +A GCP Spanner Database is a logically isolated collection of relational data that lives inside a Cloud Spanner instance. It contains the schema (tables, indexes, views) and the data itself, and it inherits the instance’s compute and storage resources. Cloud Spanner provides global consistency, horizontal scalability and automatic replication, making the database suitable for mission-critical, globally distributed workloads. Official documentation: https://cloud.google.com/spanner/docs **Terrafrom Mappings:** -- `google_spanner_database.name` + * `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" +* `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. +A Spanner database can be encrypted with a customer-managed encryption key (CMEK) stored in Cloud KMS. Overmind links the database to the KMS Crypto Key that protects its data at rest. + +### [`gcp-cloud-kms-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) + +When CMEK is enabled, Spanner actually uses a specific version of the KMS key. This link shows the exact key version currently in use so you can track key rotation and ensure compliance. + +### [`gcp-spanner-database`](/sources/gcp/Types/gcp-spanner-database) + +Spanner databases may reference one another through backups, clones or restores. Overmind records these relationships (e.g., a database restored from another) to expose any dependency chain between databases. ### [`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. +Every Spanner database belongs to a single Spanner instance. This link lets you traverse from the database to the parent instance to understand the compute resources, regional configuration and IAM policies that ultimately govern the database. \ No newline at end of file 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 index b3820f65..8d9baff5 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-instance.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-instance.md @@ -3,21 +3,21 @@ 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 +A **Cloud Spanner instance** is the top-level container that defines the geographical placement, compute capacity and billing context for one or more Cloud Spanner databases. When you create an instance you choose an instance configuration (regional or multi-regional) and allocate compute in the form of nodes or processing units; all databases created within the instance inherit this configuration and capacity. Google manages replication, automatic fail-over and online scaling transparently within the boundaries of the instance. +For full details see the official documentation: https://cloud.google.com/spanner/docs/instances **Terrafrom Mappings:** -- `google_spanner_instance.name` + * `google_spanner_instance.name` ## Supported Methods -- `GET`: Get a gcp-spanner-instance by its "name" -- `LIST`: List all gcp-spanner-instance -- ~~`SEARCH`~~ +* `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. +Each Cloud Spanner instance can contain multiple Cloud Spanner databases. The `gcp-spanner-database` resource is therefore a child of the `gcp-spanner-instance`; enumerating databases or assessing their risks starts with traversing from the parent instance to its associated databases. \ No newline at end of file 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 index 7192c072..03511392 100644 --- 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 @@ -3,20 +3,25 @@ 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 +A **Cloud SQL Backup Run** represents a single on-demand or automated backup operation for a Cloud SQL instance. It records when the backup was initiated, its status, size, location, encryption information and other metadata. Backup runs allow administrators to restore an instance to a previous state or to clone data into a new instance. +Official 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" +* `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. +If Customer-Managed Encryption Keys (CMEK) are enabled for the instance, the backup run is encrypted with a Cloud KMS Crypto Key. This link points to the parent key that protects the specific key version used for the backup. + +### [`gcp-cloud-kms-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) + +The `encryptionInfo` block inside the backup run references the exact Cloud KMS Crypto Key Version that encrypted the backup file. This relationship lets you trace which key version must be available to decrypt or restore the backup. ### [`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. +Every backup run belongs to a single Cloud SQL instance. This link connects the backup run to its parent instance so you can see which database the backup protects and assess the impact of restoring or deleting it. \ No newline at end of file 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 index 3c686d99..13145280 100644 --- 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 @@ -3,26 +3,29 @@ 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. +A **Cloud SQL backup** represents a point-in-time copy of the data stored in a Cloud SQL instance. Backups are created automatically on a schedule you define or manually on demand, and are retained in Google-managed Cloud Storage where they can later be used to restore the originating instance or clone a new one. Backups may be encrypted either with Google-managed keys or with a customer-managed encryption key (CMEK) from Cloud KMS. +See the official documentation for details: 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`~~ +* `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. +If CMEK encryption is enabled for the Cloud SQL instance, the backup is encrypted with a specific Cloud KMS CryptoKey. This link shows which key secures the backup data at rest. -### [`gcp-sql-admin-backup-run`](/sources/gcp/Types/gcp-sql-admin-backup-run) +### [`gcp-cloud-kms-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) -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. +The actual ciphertext is tied to a particular CryptoKey **version**. Linking to the key version lets you see exactly which rotation of the key was used when the backup was taken. + +### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) + +Although backups are stored out-of-band, they are associated with the same VPC network(s) as the Cloud SQL instance that produced them. This link helps trace network-level access policies that apply when a backup is restored to an instance using private IP. ### [`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. +Every backup is generated from, and can be restored to, a specific Cloud SQL instance. This link identifies the parent instance, allowing you to evaluate how instance configuration (e.g. region, database version) affects backup usability and risk. \ No newline at end of file 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 index 86a0993c..41cd6a4f 100644 --- 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 @@ -3,40 +3,44 @@ 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. +A Google Cloud SQL Admin Instance represents a fully-managed relational database instance running on Google Cloud. It encapsulates the configuration for engines such as MySQL, PostgreSQL, or SQL Server, including CPU and memory sizing, version, storage, networking and encryption settings. For full details see the official documentation: https://cloud.google.com/sql/docs/introduction. **Terrafrom Mappings:** -- `google_sql_database_instance.name` + * `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`~~ +* `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. +Linked when the instance is encrypted with a Customer-Managed Encryption Key (CMEK); the instance stores the resource ID of the Cloud KMS crypto key it uses for data-at-rest encryption. ### [`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. +Appears when the instance is configured with a private IP address. The instance is reachable through a Private Service Connection residing inside a specific VPC network. + +### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) + +If private IP is enabled, the instance is bound to a particular subnetwork from which it obtains its internal IP and through which it exposes its endpoints. ### [`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. +Cloud SQL creates or uses a service account to perform administrative actions such as backup, replication and interaction with other Google Cloud services; this link surfaces that service account. ### [`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. +Each successful or scheduled backup run is a child of an instance. The link shows all backup-run resources that belong to the current database instance. ### [`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. +An instance can reference another instance as its read replica or as the source for cloning. This self-link captures those primary/replica relationships. ### [`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. +Imports, exports and point-in-time backups can read from or write to Cloud Storage. The instance therefore maintains references to buckets used for these operations. \ No newline at end of file diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket-iam-policy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket-iam-policy.md new file mode 100644 index 00000000..506c5c49 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket-iam-policy.md @@ -0,0 +1,36 @@ +--- +title: GCP Storage Bucket Iam Policy +sidebar_label: gcp-storage-bucket-iam-policy +--- + +A **Storage Bucket IAM policy** defines who (principals) can perform which actions (roles/permissions) on a specific Cloud Storage bucket. It is the fine-grained access-control object that sits on top of a bucket and overrides or complements broader project-level IAM settings. For full details, see the Google Cloud documentation: https://cloud.google.com/storage/docs/access-control/iam + +**Terrafrom Mappings:** + + * `google_storage_bucket_iam_binding.bucket` + * `google_storage_bucket_iam_member.bucket` + * `google_storage_bucket_iam_policy.bucket` + +## Supported Methods + +* `GET`: Get GCP Storage Bucket Iam Policy by "gcp-storage-bucket-iam-policy-bucket" +* ~~`LIST`~~ +* `SEARCH`: Search for GCP Storage Bucket Iam Policy by "gcp-storage-bucket-iam-policy-bucket" + +## Possible Links + +### [`gcp-compute-project`](/sources/gcp/Types/gcp-compute-project) + +The bucket IAM policy is scoped within a single GCP project; therefore every policy item is linked back to the project that owns the bucket. + +### [`gcp-iam-role`](/sources/gcp/Types/gcp-iam-role) + +Each binding inside the policy references one or more IAM roles that grant permissions; this link shows which predefined or custom roles are in use. + +### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) + +Service accounts are common principals in bucket policies. Linking reveals which service accounts have been granted access and with what privileges. + +### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) + +The IAM policy is attached to and governs a specific Cloud Storage bucket; this link connects the policy object to the underlying bucket resource. \ No newline at end of file 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 index a4b4edde..1bd3e48f 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket.md @@ -3,28 +3,36 @@ 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 +A Google Cloud Storage Bucket is a globally-unique container used to store, organise and serve objects (files) in Google Cloud Storage. Buckets provide configuration points for data location, access control, lifecycle management, encryption and logging. They are the fundamental resource for object storage workloads such as static website hosting, backup, or data lakes. +For full details see the official documentation: https://cloud.google.com/storage/docs/buckets **Terrafrom Mappings:** -- `google_storage_bucket.name` + * `google_storage_bucket.name` + * `google_storage_bucket_iam_binding.bucket` + * `google_storage_bucket_iam_member.bucket` + * `google_storage_bucket_iam_policy.bucket` ## Supported Methods -- `GET`: Get a gcp-storage-bucket by its "name" -- `LIST`: List all gcp-storage-bucket -- ~~`SEARCH`~~ +* `GET`: Get a gcp-storage-bucket by its "name" +* `LIST`: List all gcp-storage-bucket +* ~~`SEARCH`~~ ## Possible Links +### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) + +A bucket may be encrypted with a customer-managed encryption key (CMEK) that resides in Cloud KMS. The bucket’s encryption configuration therefore references the corresponding `gcp-cloud-kms-crypto-key`. + ### [`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. +When VPC Service Controls or Private Google Access are used, access between a Compute Network and a Storage Bucket is constrained or allowed based on network settings. Log sinks from VPC flow logs can also target a Storage Bucket, creating a relationship between the bucket and the originating `gcp-compute-network`. ### [`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. +Cloud Logging can route logs from a Logging Bucket to Cloud Storage for long-term retention or auditing. If such a sink targets this Storage Bucket, the bucket becomes linked to the source `gcp-logging-bucket`. -### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) +### [`gcp-storage-bucket-iam-policy`](/sources/gcp/Types/gcp-storage-bucket-iam-policy) -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. +Every Storage Bucket has an IAM policy that defines who can read, write or administer it. That policy is exposed as a separate `gcp-storage-bucket-iam-policy` object, which is directly attached to this bucket. \ No newline at end of file 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 index 01476fc4..c0836910 100644 --- 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 @@ -3,32 +3,37 @@ 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 +Google Cloud Storage Transfer Service enables you to copy or synchronise data between Cloud Storage buckets, on-premises file systems and external cloud providers. A Storage Transfer **transfer job** is the top-level resource that defines where data should be copied from, where it should be copied to, the schedule on which the copy should run, and options such as delete or overwrite rules. +Official documentation: https://cloud.google.com/storage-transfer/docs/create-transfers **Terrafrom Mappings:** -- `google_storage_transfer_job.name` + * `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`~~ +* `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. +The transfer job runs under a Google-managed or user-specified IAM service account, which needs roles such as `Storage Object Admin` on the destination bucket and, when applicable, permissions to access the source. ### [`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. +If event notifications are enabled, a Pub/Sub subscription can pull the messages that the transfer job publishes when it starts, completes, or encounters errors. ### [`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. +A transfer job can be configured with a Pub/Sub topic as its notification destination so that operational events are published for downstream processing or alerting. + +### [`gcp-secret-manager-secret`](/sources/gcp/Types/gcp-secret-manager-secret) + +When transferring from external providers such as AWS S3 or Azure Blob Storage, the access keys and credentials are often stored in Secret Manager secrets, which the transfer job references to authenticate to the source. ### [`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. +Every transfer job specifies at least one Cloud Storage bucket as a source and/or destination; therefore it has direct relationships to the buckets involved in the data copy. \ No newline at end of file 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 index 967b9b4b..533f721d 100644 --- 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 @@ -2,9 +2,11 @@ "type": "gcp-ai-platform-batch-prediction-job", "category": 8, "potentialLinks": [ + "gcp-ai-platform-endpoint", "gcp-ai-platform-model", "gcp-big-query-table", "gcp-cloud-kms-crypto-key", + "gcp-compute-network", "gcp-iam-service-account", "gcp-storage-bucket" ], @@ -15,4 +17,4 @@ "search": true, "searchDescription": "Search Batch Prediction Jobs within a location. Use the location name e.g., 'us-central1'" } -} +} \ No newline at end of file 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 index 22003f70..178705cb 100644 --- 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 @@ -3,6 +3,7 @@ "category": 8, "potentialLinks": [ "gcp-ai-platform-model", + "gcp-artifact-registry-docker-image", "gcp-cloud-kms-crypto-key", "gcp-compute-network", "gcp-iam-service-account", @@ -15,4 +16,4 @@ "list": true, "listDescription": "List all gcp-ai-platform-custom-job" } -} +} \ No newline at end of file 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 index 67f77a9c..9bb17e71 100644 --- 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 @@ -6,7 +6,8 @@ "gcp-ai-platform-model-deployment-monitoring-job", "gcp-big-query-table", "gcp-cloud-kms-crypto-key", - "gcp-compute-network" + "gcp-compute-network", + "gcp-iam-service-account" ], "descriptiveName": "GCP Ai Platform Endpoint", "supportedQueryMethods": { @@ -15,4 +16,4 @@ "list": true, "listDescription": "List all gcp-ai-platform-endpoint" } -} +} \ No newline at end of file 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 index 5d1e6d04..5f618ed7 100644 --- 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 @@ -4,8 +4,10 @@ "potentialLinks": [ "gcp-ai-platform-endpoint", "gcp-ai-platform-model", + "gcp-big-query-table", "gcp-cloud-kms-crypto-key", - "gcp-monitoring-notification-channel" + "gcp-monitoring-notification-channel", + "gcp-storage-bucket" ], "descriptiveName": "GCP Ai Platform Model Deployment Monitoring Job", "supportedQueryMethods": { @@ -14,4 +16,4 @@ "search": true, "searchDescription": "Search Model Deployment Monitoring Jobs within a location. Use the location name e.g., 'us-central1'" } -} +} \ No newline at end of file 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 index a0ccd633..24c3254c 100644 --- 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 @@ -5,7 +5,8 @@ "gcp-ai-platform-endpoint", "gcp-ai-platform-pipeline-job", "gcp-artifact-registry-docker-image", - "gcp-cloud-kms-crypto-key" + "gcp-cloud-kms-crypto-key", + "gcp-storage-bucket" ], "descriptiveName": "GCP Ai Platform Model", "supportedQueryMethods": { @@ -14,4 +15,4 @@ "list": true, "listDescription": "List all gcp-ai-platform-model" } -} +} \ No newline at end of file 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 index 48d48bc7..ebc9dbf7 100644 --- 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 @@ -14,4 +14,4 @@ "list": true, "listDescription": "List all gcp-ai-platform-pipeline-job" } -} +} \ No newline at end of file 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 index 1c51945e..f6430a93 100644 --- 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 @@ -14,4 +14,4 @@ "terraformQueryMap": "google_artifact_registry_docker_image.name" } ] -} +} \ No newline at end of file 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 index fa7b6a88..00e50fce 100644 --- 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 @@ -4,6 +4,7 @@ "potentialLinks": [ "gcp-big-query-dataset", "gcp-cloud-kms-crypto-key", + "gcp-iam-service-account", "gcp-pub-sub-topic" ], "descriptiveName": "GCP Big Query Data Transfer Transfer Config", @@ -19,4 +20,4 @@ "terraformQueryMap": "google_bigquery_data_transfer_config.id" } ] -} +} \ No newline at end of file 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 index 3efc5539..348cb790 100644 --- 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 @@ -3,7 +3,6 @@ "category": 6, "potentialLinks": [ "gcp-big-query-dataset", - "gcp-big-query-model", "gcp-big-query-routine", "gcp-big-query-table", "gcp-cloud-kms-crypto-key", @@ -19,6 +18,15 @@ "terraformMappings": [ { "terraformQueryMap": "google_bigquery_dataset.dataset_id" + }, + { + "terraformQueryMap": "google_bigquery_dataset_iam_binding.dataset_id" + }, + { + "terraformQueryMap": "google_bigquery_dataset_iam_member.dataset_id" + }, + { + "terraformQueryMap": "google_bigquery_dataset_iam_policy.dataset_id" } ] -} +} \ No newline at end of file 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 deleted file mode 100644 index be1751c4..00000000 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-big-query-model.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "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 index b9704125..25731ea9 100644 --- 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 @@ -1,7 +1,10 @@ { "type": "gcp-big-query-routine", "category": 6, - "potentialLinks": ["gcp-big-query-dataset"], + "potentialLinks": [ + "gcp-big-query-dataset", + "gcp-storage-bucket" + ], "descriptiveName": "GCP Big Query Routine", "supportedQueryMethods": { "get": true, @@ -11,7 +14,8 @@ }, "terraformMappings": [ { - "terraformQueryMap": "google_bigquery_routine.routine_id" + "terraformMethod": 2, + "terraformQueryMap": "google_bigquery_routine.id" } ] -} +} \ No newline at end of file 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 index 6c9b01c6..cbb926c0 100644 --- 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 @@ -1,7 +1,12 @@ { "type": "gcp-big-query-table", "category": 6, - "potentialLinks": ["gcp-big-query-dataset", "gcp-cloud-kms-crypto-key"], + "potentialLinks": [ + "gcp-big-query-dataset", + "gcp-big-query-table", + "gcp-cloud-kms-crypto-key", + "gcp-storage-bucket" + ], "descriptiveName": "GCP Big Query Table", "supportedQueryMethods": { "get": true, @@ -11,7 +16,20 @@ }, "terraformMappings": [ { + "terraformMethod": 2, "terraformQueryMap": "google_bigquery_table.id" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "google_bigquery_table_iam_binding.dataset_id" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "google_bigquery_table_iam_member.dataset_id" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "google_bigquery_table_iam_policy.dataset_id" } ] -} +} \ No newline at end of file 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 index 4358f119..c6c3e240 100644 --- 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 @@ -18,4 +18,4 @@ "terraformQueryMap": "google_bigtable_app_profile.id" } ] -} +} \ No newline at end of file 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 index 3f8518c8..bc06d5ee 100644 --- 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 @@ -3,7 +3,8 @@ "potentialLinks": [ "gcp-big-table-admin-backup", "gcp-big-table-admin-cluster", - "gcp-big-table-admin-table" + "gcp-big-table-admin-table", + "gcp-cloud-kms-crypto-key-version" ], "descriptiveName": "GCP Big Table Admin Backup", "supportedQueryMethods": { @@ -12,4 +13,4 @@ "search": true, "searchDescription": "Search for gcp-big-table-admin-backup by its \"instances|clusters\"" } -} +} \ No newline at end of file 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 index bb3be7f6..37d238de 100644 --- 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 @@ -12,4 +12,4 @@ "search": true, "searchDescription": "Search for gcp-big-table-admin-cluster by its \"instances\"" } -} +} \ No newline at end of file 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 index fabaff46..fff15c10 100644 --- 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 @@ -1,7 +1,9 @@ { "type": "gcp-big-table-admin-instance", "category": 7, - "potentialLinks": ["gcp-big-table-admin-cluster"], + "potentialLinks": [ + "gcp-big-table-admin-cluster" + ], "descriptiveName": "GCP Big Table Admin Instance", "supportedQueryMethods": { "get": true, @@ -12,6 +14,15 @@ "terraformMappings": [ { "terraformQueryMap": "google_bigtable_instance.name" + }, + { + "terraformQueryMap": "google_bigtable_instance_iam_binding.instance" + }, + { + "terraformQueryMap": "google_bigtable_instance_iam_member.instance" + }, + { + "terraformQueryMap": "google_bigtable_instance_iam_policy.instance" } ] -} +} \ No newline at end of file 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 index f490a956..c24314d6 100644 --- 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 @@ -17,6 +17,18 @@ { "terraformMethod": 2, "terraformQueryMap": "google_bigtable_table.id" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "google_bigtable_table_iam_binding.instance_name" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "google_bigtable_table_iam_member.instance_name" + }, + { + "terraformMethod": 2, + "terraformQueryMap": "google_bigtable_table_iam_policy.instance_name" } ] -} +} \ No newline at end of file diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-certificate-manager-certificate.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-certificate-manager-certificate.json new file mode 100644 index 00000000..0a31cbc1 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-certificate-manager-certificate.json @@ -0,0 +1,17 @@ +{ + "type": "gcp-certificate-manager-certificate", + "category": 4, + "descriptiveName": "GCP Certificate Manager Certificate", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Certificate Manager Certificate by \"gcp-certificate-manager-certificate-location|gcp-certificate-manager-certificate-name\"", + "search": true, + "searchDescription": "Search for GCP Certificate Manager Certificate by \"gcp-certificate-manager-certificate-location\"" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_certificate_manager_certificate.id" + } + ] +} \ No newline at end of file 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 index d34a018f..8d13c29c 100644 --- 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 @@ -1,10 +1,12 @@ { "type": "gcp-cloud-billing-billing-info", "category": 7, - "potentialLinks": ["gcp-cloud-resource-manager-project"], + "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\"" } -} +} \ No newline at end of file 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 index 234cabf0..6e88a687 100644 --- 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 @@ -3,8 +3,10 @@ "category": 7, "potentialLinks": [ "gcp-artifact-registry-docker-image", + "gcp-cloud-kms-crypto-key", "gcp-iam-service-account", "gcp-logging-bucket", + "gcp-secret-manager-secret", "gcp-storage-bucket" ], "descriptiveName": "GCP Cloud Build Build", @@ -14,4 +16,4 @@ "list": true, "listDescription": "List all gcp-cloud-build-build" } -} +} \ No newline at end of file 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 index aa542ad7..8c31985f 100644 --- 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 @@ -15,4 +15,4 @@ "search": true, "searchDescription": "Search for gcp-cloud-functions-function by its \"locations\"" } -} +} \ No newline at end of file diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key-version.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key-version.json new file mode 100644 index 00000000..5f931fe5 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key-version.json @@ -0,0 +1,20 @@ +{ + "type": "gcp-cloud-kms-crypto-key-version", + "category": 4, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key" + ], + "descriptiveName": "GCP Cloud Kms Crypto Key Version", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Cloud Kms Crypto Key Version by \"gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name|gcp-cloud-kms-crypto-key-name|gcp-cloud-kms-crypto-key-version-version\"", + "search": true, + "searchDescription": "Search for GCP Cloud Kms Crypto Key Version by \"gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name|gcp-cloud-kms-crypto-key-name\"" + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_kms_crypto_key_version.id" + } + ] +} \ No newline at end of file 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 index c28ec0de..1995c3de 100644 --- 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 @@ -1,12 +1,21 @@ { "type": "gcp-cloud-kms-crypto-key", "category": 4, - "potentialLinks": ["gcp-cloud-kms-key-ring"], + "potentialLinks": [ + "gcp-cloud-kms-crypto-key-version", + "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\"" - } -} + }, + "terraformMappings": [ + { + "terraformMethod": 2, + "terraformQueryMap": "google_kms_crypto_key.id" + } + ] +} \ No newline at end of file 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 index 6eeaa47c..c2376a47 100644 --- 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 @@ -1,17 +1,22 @@ { "type": "gcp-cloud-kms-key-ring", "category": 4, - "potentialLinks": ["gcp-cloud-kms-crypto-key"], + "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\"", + "list": true, + "listDescription": "List all GCP Cloud Kms Key Ring items", "search": true, "searchDescription": "Search for GCP Cloud Kms Key Ring by \"gcp-cloud-kms-key-ring-location\"" }, "terraformMappings": [ { - "terraformQueryMap": "google_kms_key_ring.name" + "terraformMethod": 2, + "terraformQueryMap": "google_kms_key_ring.id" } ] -} +} \ No newline at end of file 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 index 61a6eb0b..4e2ae5cf 100644 --- 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 @@ -6,4 +6,4 @@ "get": true, "getDescription": "Get a gcp-cloud-resource-manager-project by its \"name\"" } -} +} \ No newline at end of file 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 index 48c3491c..1edebcde 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_tags_tag_value.name" } ] -} +} \ No newline at end of file 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 index 53ebe1ef..b15037cb 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-address.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-address.json @@ -3,7 +3,12 @@ "category": 3, "potentialLinks": [ "gcp-compute-address", + "gcp-compute-forwarding-rule", + "gcp-compute-global-forwarding-rule", + "gcp-compute-instance", "gcp-compute-network", + "gcp-compute-public-delegated-prefix", + "gcp-compute-router", "gcp-compute-subnetwork" ], "descriptiveName": "GCP Compute Address", @@ -18,4 +23,4 @@ "terraformQueryMap": "google_compute_address.name" } ] -} +} \ No newline at end of file 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 index 41effcaf..79401514 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-autoscaler.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-autoscaler.json @@ -1,7 +1,9 @@ { "type": "gcp-compute-autoscaler", "category": 7, - "potentialLinks": ["gcp-compute-instance-group-manager"], + "potentialLinks": [ + "gcp-compute-instance-group-manager" + ], "descriptiveName": "GCP Compute Autoscaler", "supportedQueryMethods": { "get": true, @@ -14,4 +16,4 @@ "terraformQueryMap": "google_compute_autoscaler.name" } ] -} +} \ No newline at end of file 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 index c103843e..69e07b51 100644 --- 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 @@ -1,7 +1,14 @@ { "type": "gcp-compute-backend-service", "category": 1, - "potentialLinks": ["gcp-compute-network", "gcp-compute-security-policy"], + "potentialLinks": [ + "gcp-compute-health-check", + "gcp-compute-instance", + "gcp-compute-instance-group", + "gcp-compute-network", + "gcp-compute-network-endpoint-group", + "gcp-compute-security-policy" + ], "descriptiveName": "GCP Compute Backend Service", "supportedQueryMethods": { "get": true, @@ -12,6 +19,9 @@ "terraformMappings": [ { "terraformQueryMap": "google_compute_backend_service.name" + }, + { + "terraformQueryMap": "google_compute_region_backend_service.name" } ] -} +} \ No newline at end of file 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 index 435b2c8c..03c34339 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-disk.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-disk.json @@ -2,11 +2,13 @@ "type": "gcp-compute-disk", "category": 2, "potentialLinks": [ + "gcp-cloud-kms-crypto-key-version", "gcp-compute-disk", "gcp-compute-image", "gcp-compute-instance", "gcp-compute-instant-snapshot", - "gcp-compute-snapshot" + "gcp-compute-snapshot", + "gcp-storage-bucket" ], "descriptiveName": "GCP Compute Disk", "supportedQueryMethods": { @@ -20,4 +22,4 @@ "terraformQueryMap": "google_compute_disk.name" } ] -} +} \ No newline at end of file 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 index 2ee555ba..644702b3 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_compute_external_vpn_gateway.name" } ] -} +} \ No newline at end of file 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 index 6dd11c29..9f7b4208 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-firewall.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-firewall.json @@ -1,17 +1,23 @@ { "type": "gcp-compute-firewall", "category": 3, - "potentialLinks": ["gcp-compute-network", "gcp-iam-service-account"], + "potentialLinks": [ + "gcp-compute-instance", + "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" + "listDescription": "List all gcp-compute-firewall", + "search": true, + "searchDescription": "Search for firewalls by network tag. The query is a plain network tag name." }, "terraformMappings": [ { "terraformQueryMap": "google_compute_firewall.name" } ] -} +} \ No newline at end of file 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 index bd2ce7de..5342f4f2 100644 --- 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 @@ -3,8 +3,13 @@ "category": 3, "potentialLinks": [ "gcp-compute-backend-service", + "gcp-compute-forwarding-rule", "gcp-compute-network", - "gcp-compute-subnetwork" + "gcp-compute-public-delegated-prefix", + "gcp-compute-subnetwork", + "gcp-compute-target-http-proxy", + "gcp-compute-target-https-proxy", + "gcp-compute-target-pool" ], "descriptiveName": "GCP Compute Forwarding Rule", "supportedQueryMethods": { @@ -18,4 +23,4 @@ "terraformQueryMap": "google_compute_forwarding_rule.name" } ] -} +} \ No newline at end of file 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 index 84db32de..0072832c 100644 --- 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 @@ -1,7 +1,11 @@ { "type": "gcp-compute-global-address", "category": 3, - "potentialLinks": ["gcp-compute-network"], + "potentialLinks": [ + "gcp-compute-network", + "gcp-compute-public-delegated-prefix", + "gcp-compute-subnetwork" + ], "descriptiveName": "GCP Compute Global Address", "supportedQueryMethods": { "get": true, @@ -14,4 +18,4 @@ "terraformQueryMap": "google_compute_global_address.name" } ] -} +} \ No newline at end of file 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 index 28c37fc0..e1125106 100644 --- 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 @@ -4,7 +4,8 @@ "potentialLinks": [ "gcp-compute-backend-service", "gcp-compute-network", - "gcp-compute-subnetwork" + "gcp-compute-subnetwork", + "gcp-compute-target-http-proxy" ], "descriptiveName": "GCP Compute Global Forwarding Rule", "supportedQueryMethods": { @@ -18,4 +19,4 @@ "terraformQueryMap": "google_compute_global_forwarding_rule.name" } ] -} +} \ No newline at end of file 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 index beda2c95..cdeafd85 100644 --- 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 @@ -11,6 +11,9 @@ "terraformMappings": [ { "terraformQueryMap": "google_compute_health_check.name" + }, + { + "terraformQueryMap": "google_compute_region_health_check.name" } ] -} +} \ No newline at end of file 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 index f5d21e72..1965aa77 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_compute_http_health_check.name" } ] -} +} \ No newline at end of file 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 index acf4c7a2..fdd276a7 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-image.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-image.json @@ -1,16 +1,27 @@ { "type": "gcp-compute-image", "category": 1, + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-cloud-kms-crypto-key-version", + "gcp-compute-disk", + "gcp-compute-image", + "gcp-compute-snapshot", + "gcp-iam-service-account", + "gcp-storage-bucket" + ], "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" + "listDescription": "List all GCP Compute Image items", + "search": true, + "searchDescription": "Search for GCP Compute Image by \"gcp-compute-image-family\"" }, "terraformMappings": [ { "terraformQueryMap": "google_compute_image.name" } ] -} +} \ No newline at end of file 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 index bc80be40..3c6f5b4d 100644 --- 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 @@ -3,6 +3,7 @@ "category": 1, "potentialLinks": [ "gcp-compute-autoscaler", + "gcp-compute-health-check", "gcp-compute-instance-group", "gcp-compute-instance-template", "gcp-compute-target-pool" @@ -19,4 +20,4 @@ "terraformQueryMap": "google_compute_instance_group_manager.name" } ] -} +} \ No newline at end of file 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 index 0368fa88..72795236 100644 --- 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 @@ -1,7 +1,10 @@ { "type": "gcp-compute-instance-group", "category": 1, - "potentialLinks": ["gcp-compute-network", "gcp-compute-subnetwork"], + "potentialLinks": [ + "gcp-compute-network", + "gcp-compute-subnetwork" + ], "descriptiveName": "GCP Compute Instance Group", "supportedQueryMethods": { "get": true, @@ -14,4 +17,4 @@ "terraformQueryMap": "google_compute_instance_group.name" } ] -} +} \ No newline at end of file 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 index b56bff5c..864330b3 100644 --- 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 @@ -4,11 +4,13 @@ "potentialLinks": [ "gcp-cloud-kms-crypto-key", "gcp-compute-disk", + "gcp-compute-firewall", "gcp-compute-image", "gcp-compute-instance", "gcp-compute-network", "gcp-compute-node-group", "gcp-compute-reservation", + "gcp-compute-route", "gcp-compute-security-policy", "gcp-compute-snapshot", "gcp-compute-subnetwork", @@ -19,11 +21,13 @@ "get": true, "getDescription": "Get a gcp-compute-instance-template by its \"name\"", "list": true, - "listDescription": "List all gcp-compute-instance-template" + "listDescription": "List all gcp-compute-instance-template", + "search": true, + "searchDescription": "Search for instance templates by network tag. The query is a plain network tag name." }, "terraformMappings": [ { "terraformQueryMap": "google_compute_instance_template.name" } ] -} +} \ No newline at end of file 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 index b4700f94..0f1b04ff 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance.json @@ -2,20 +2,31 @@ "type": "gcp-compute-instance", "category": 1, "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-cloud-kms-crypto-key-version", "gcp-compute-disk", + "gcp-compute-firewall", + "gcp-compute-image", + "gcp-compute-instance-group-manager", + "gcp-compute-instance-template", "gcp-compute-network", - "gcp-compute-subnetwork" + "gcp-compute-route", + "gcp-compute-snapshot", + "gcp-compute-subnetwork", + "gcp-iam-service-account" ], "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" + "listDescription": "List all GCP Compute Instance items", + "search": true, + "searchDescription": "Search for GCP Compute Instance by \"gcp-compute-instance-networkTag\"" }, "terraformMappings": [ { "terraformQueryMap": "google_compute_instance.name" } ] -} +} \ No newline at end of file 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 index a37e8bd6..f0007c97 100644 --- 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 @@ -1,7 +1,9 @@ { "type": "gcp-compute-instant-snapshot", "category": 2, - "potentialLinks": ["gcp-compute-disk"], + "potentialLinks": [ + "gcp-compute-disk" + ], "descriptiveName": "GCP Compute Instant Snapshot", "supportedQueryMethods": { "get": true, @@ -14,4 +16,4 @@ "terraformQueryMap": "google_compute_instant_snapshot.name" } ] -} +} \ No newline at end of file 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 index 72970664..6ae291c0 100644 --- 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 @@ -2,10 +2,14 @@ "type": "gcp-compute-machine-image", "category": 1, "potentialLinks": [ + "gcp-cloud-kms-crypto-key-version", "gcp-compute-disk", + "gcp-compute-image", "gcp-compute-instance", "gcp-compute-network", - "gcp-compute-subnetwork" + "gcp-compute-snapshot", + "gcp-compute-subnetwork", + "gcp-iam-service-account" ], "descriptiveName": "GCP Compute Machine Image", "supportedQueryMethods": { @@ -19,4 +23,4 @@ "terraformQueryMap": "google_compute_machine_image.name" } ] -} +} \ No newline at end of file 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 index cf4fa2ae..cbe1024e 100644 --- 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 @@ -19,4 +19,4 @@ "terraformQueryMap": "google_compute_network_endpoint_group.name" } ] -} +} \ No newline at end of file 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 index f4e5d730..76e7a4d7 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network.json @@ -1,7 +1,10 @@ { "type": "gcp-compute-network", "category": 3, - "potentialLinks": ["gcp-compute-network", "gcp-compute-subnetwork"], + "potentialLinks": [ + "gcp-compute-network", + "gcp-compute-subnetwork" + ], "descriptiveName": "GCP Compute Network", "supportedQueryMethods": { "get": true, @@ -14,4 +17,4 @@ "terraformQueryMap": "google_compute_network.name" } ] -} +} \ No newline at end of file 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 index 73f3c8c0..9787e141 100644 --- 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 @@ -1,6 +1,9 @@ { "type": "gcp-compute-node-group", "category": 1, + "potentialLinks": [ + "gcp-compute-node-template" + ], "descriptiveName": "GCP Compute Node Group", "supportedQueryMethods": { "get": true, @@ -19,4 +22,4 @@ "terraformQueryMap": "google_compute_node_template.name" } ] -} +} \ No newline at end of file diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-template.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-template.json new file mode 100644 index 00000000..b488d338 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-template.json @@ -0,0 +1,19 @@ +{ + "type": "gcp-compute-node-template", + "category": 7, + "potentialLinks": [ + "gcp-compute-node-group" + ], + "descriptiveName": "GCP Compute Node Template", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Node Template by \"gcp-compute-node-template-name\"", + "list": true, + "listDescription": "List all GCP Compute Node Template items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_node_template.name" + } + ] +} \ No newline at end of file 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 index b6c4fd2b..55885cc9 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-project.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-project.json @@ -1,10 +1,39 @@ { "type": "gcp-compute-project", "category": 7, - "potentialLinks": ["gcp-iam-service-account", "gcp-storage-bucket"], + "potentialLinks": [ + "gcp-iam-service-account", + "gcp-storage-bucket" + ], "descriptiveName": "GCP Compute Project", "supportedQueryMethods": { "get": true, "getDescription": "Get a gcp-compute-project by its \"name\"" - } -} + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_project.project_id" + }, + { + "terraformQueryMap": "google_compute_shared_vpc_host_project.project" + }, + { + "terraformQueryMap": "google_compute_shared_vpc_service_project.service_project" + }, + { + "terraformQueryMap": "google_compute_shared_vpc_service_project.host_project" + }, + { + "terraformQueryMap": "google_project_iam_binding.project" + }, + { + "terraformQueryMap": "google_project_iam_member.project" + }, + { + "terraformQueryMap": "google_project_iam_policy.project" + }, + { + "terraformQueryMap": "google_project_iam_audit_config.project" + } + ] +} \ No newline at end of file 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 index 7348176c..e213916a 100644 --- 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 @@ -20,4 +20,4 @@ "terraformQueryMap": "google_compute_public_delegated_prefix.id" } ] -} +} \ No newline at end of file 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 deleted file mode 100644 index 81449263..00000000 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-region-backend-service.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "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 index 56244bf7..c9862743 100644 --- 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 @@ -1,6 +1,8 @@ { "type": "gcp-compute-region-commitment", - "potentialLinks": ["gcp-compute-reservation"], + "potentialLinks": [ + "gcp-compute-reservation" + ], "descriptiveName": "GCP Compute Region Commitment", "supportedQueryMethods": { "get": true, @@ -13,4 +15,4 @@ "terraformQueryMap": "google_compute_region_commitment.name" } ] -} +} \ No newline at end of file diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-regional-instance-group-manager.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-regional-instance-group-manager.json new file mode 100644 index 00000000..802da3df --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-regional-instance-group-manager.json @@ -0,0 +1,23 @@ +{ + "type": "gcp-compute-regional-instance-group-manager", + "category": 1, + "potentialLinks": [ + "gcp-compute-autoscaler", + "gcp-compute-health-check", + "gcp-compute-instance-group", + "gcp-compute-instance-template", + "gcp-compute-target-pool" + ], + "descriptiveName": "GCP Compute Regional Instance Group Manager", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Compute Regional Instance Group Manager by \"gcp-compute-regional-instance-group-manager-name\"", + "list": true, + "listDescription": "List all GCP Compute Regional Instance Group Manager items" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_compute_region_instance_group_manager.name" + } + ] +} \ No newline at end of file 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 index 6ea4f52b..94e395e6 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-reservation.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-reservation.json @@ -16,4 +16,4 @@ "terraformQueryMap": "google_compute_reservation.name" } ] -} +} \ No newline at end of file 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 index 5a2ebefc..4172966d 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-route.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-route.json @@ -2,6 +2,7 @@ "type": "gcp-compute-route", "category": 3, "potentialLinks": [ + "gcp-compute-forwarding-rule", "gcp-compute-instance", "gcp-compute-network", "gcp-compute-vpn-tunnel" @@ -11,11 +12,13 @@ "get": true, "getDescription": "Get a gcp-compute-route by its \"name\"", "list": true, - "listDescription": "List all gcp-compute-route" + "listDescription": "List all gcp-compute-route", + "search": true, + "searchDescription": "Search for routes by network tag. The query is a plain network tag name." }, "terraformMappings": [ { "terraformQueryMap": "google_compute_route.name" } ] -} +} \ No newline at end of file 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 index 525bec37..962ebed7 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-router.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-router.json @@ -21,4 +21,4 @@ "terraformQueryMap": "google_compute_router.id" } ] -} +} \ No newline at end of file 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 index b2a5b7e5..2c451384 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_compute_security_policy.name" } ] -} +} \ No newline at end of file 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 index 5b42d808..8bd46da1 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-snapshot.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-snapshot.json @@ -1,7 +1,11 @@ { "type": "gcp-compute-snapshot", "category": 2, - "potentialLinks": ["gcp-compute-disk", "gcp-compute-instant-snapshot"], + "potentialLinks": [ + "gcp-cloud-kms-crypto-key-version", + "gcp-compute-disk", + "gcp-compute-instant-snapshot" + ], "descriptiveName": "GCP Compute Snapshot", "supportedQueryMethods": { "get": true, @@ -14,4 +18,4 @@ "terraformQueryMap": "google_compute_snapshot.name" } ] -} +} \ No newline at end of file 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 index d2b248fc..289ad656 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_compute_ssl_certificate.name" } ] -} +} \ No newline at end of file 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 index 37962175..842004a7 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_compute_ssl_policy.name" } ] -} +} \ No newline at end of file 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 index 2af0ff8e..50a6ae34 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-subnetwork.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-subnetwork.json @@ -1,7 +1,10 @@ { "type": "gcp-compute-subnetwork", "category": 3, - "potentialLinks": ["gcp-compute-network"], + "potentialLinks": [ + "gcp-compute-network", + "gcp-compute-public-delegated-prefix" + ], "descriptiveName": "GCP Compute Subnetwork", "supportedQueryMethods": { "get": true, @@ -14,4 +17,4 @@ "terraformQueryMap": "google_compute_subnetwork.name" } ] -} +} \ No newline at end of file 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 index 866c2fe2..5243798c 100644 --- 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 @@ -1,7 +1,9 @@ { "type": "gcp-compute-target-http-proxy", "category": 3, - "potentialLinks": ["gcp-compute-url-map"], + "potentialLinks": [ + "gcp-compute-url-map" + ], "descriptiveName": "GCP Compute Target Http Proxy", "supportedQueryMethods": { "get": true, @@ -14,4 +16,4 @@ "terraformQueryMap": "google_compute_target_http_proxy.name" } ] -} +} \ No newline at end of file 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 index 329ff140..091c7fda 100644 --- 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 @@ -18,4 +18,4 @@ "terraformQueryMap": "google_compute_target_https_proxy.name" } ] -} +} \ No newline at end of file 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 index d4af55ad..71790f0f 100644 --- 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 @@ -21,4 +21,4 @@ "terraformQueryMap": "google_compute_target_pool.id" } ] -} +} \ No newline at end of file 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 index e5ae1cc4..d070939b 100644 --- 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 @@ -1,7 +1,9 @@ { "type": "gcp-compute-url-map", "category": 3, - "potentialLinks": ["gcp-compute-backend-service"], + "potentialLinks": [ + "gcp-compute-backend-service" + ], "descriptiveName": "GCP Compute Url Map", "supportedQueryMethods": { "get": true, @@ -14,4 +16,4 @@ "terraformQueryMap": "google_compute_url_map.name" } ] -} +} \ No newline at end of file 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 index 29de0dae..6cb2f48d 100644 --- 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 @@ -1,7 +1,9 @@ { "type": "gcp-compute-vpn-gateway", "category": 3, - "potentialLinks": ["gcp-compute-network"], + "potentialLinks": [ + "gcp-compute-network" + ], "descriptiveName": "GCP Compute Vpn Gateway", "supportedQueryMethods": { "get": true, @@ -14,4 +16,4 @@ "terraformQueryMap": "google_compute_ha_vpn_gateway.name" } ] -} +} \ No newline at end of file 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 index 10d8198b..ddcc1aec 100644 --- 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 @@ -18,4 +18,4 @@ "terraformQueryMap": "google_compute_vpn_tunnel.name" } ] -} +} \ No newline at end of file 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 index 75607613..31891141 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-container-cluster.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-container-cluster.json @@ -2,7 +2,9 @@ "type": "gcp-container-cluster", "category": 1, "potentialLinks": [ + "gcp-big-query-dataset", "gcp-cloud-kms-crypto-key", + "gcp-cloud-kms-crypto-key-version", "gcp-compute-network", "gcp-compute-node-group", "gcp-compute-subnetwork", @@ -23,4 +25,4 @@ "terraformQueryMap": "google_container_cluster.id" } ] -} +} \ No newline at end of file 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 index d14c9319..a50aa39e 100644 --- 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 @@ -3,7 +3,10 @@ "category": 1, "potentialLinks": [ "gcp-cloud-kms-crypto-key", + "gcp-compute-instance-group-manager", + "gcp-compute-network", "gcp-compute-node-group", + "gcp-compute-subnetwork", "gcp-container-cluster", "gcp-iam-service-account" ], @@ -20,4 +23,4 @@ "terraformQueryMap": "google_container_node_pool.id" } ] -} +} \ No newline at end of file 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 index 0513d8c6..ffba7ab9 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataform-repository.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataform-repository.json @@ -3,6 +3,7 @@ "category": 6, "potentialLinks": [ "gcp-cloud-kms-crypto-key", + "gcp-cloud-kms-crypto-key-version", "gcp-iam-service-account", "gcp-secret-manager-secret" ], @@ -19,4 +20,4 @@ "terraformQueryMap": "google_dataform_repository.id" } ] -} +} \ No newline at end of file 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 index 9d600015..8bb7d290 100644 --- 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 @@ -14,4 +14,4 @@ "terraformQueryMap": "google_dataplex_aspect_type.id" } ] -} +} \ No newline at end of file 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 index 99ae75d0..ddd66d24 100644 --- 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 @@ -1,7 +1,10 @@ { "type": "gcp-dataplex-data-scan", "category": 5, - "potentialLinks": ["gcp-storage-bucket"], + "potentialLinks": [ + "gcp-big-query-table", + "gcp-storage-bucket" + ], "descriptiveName": "GCP Dataplex Data Scan", "supportedQueryMethods": { "get": true, @@ -15,4 +18,4 @@ "terraformQueryMap": "google_dataplex_datascan.id" } ] -} +} \ No newline at end of file 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 index 22f88da0..1ab6ee27 100644 --- 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 @@ -14,4 +14,4 @@ "terraformQueryMap": "google_dataplex_entry_group.id" } ] -} +} \ No newline at end of file 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 index 05e71e51..efa1b07d 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_dataproc_autoscaling_policy.name" } ] -} +} \ No newline at end of file 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 index d1f19d26..0482caa7 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-cluster.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-cluster.json @@ -8,7 +8,10 @@ "gcp-compute-network", "gcp-compute-node-group", "gcp-compute-subnetwork", + "gcp-container-cluster", + "gcp-container-node-pool", "gcp-dataproc-autoscaling-policy", + "gcp-dataproc-cluster", "gcp-iam-service-account", "gcp-storage-bucket" ], @@ -24,4 +27,4 @@ "terraformQueryMap": "google_dataproc_cluster.name" } ] -} +} \ No newline at end of file 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 index 54e61a26..98f8ab9d 100644 --- 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 @@ -1,7 +1,10 @@ { "type": "gcp-dns-managed-zone", "category": 3, - "potentialLinks": ["gcp-compute-network", "gcp-container-cluster"], + "potentialLinks": [ + "gcp-compute-network", + "gcp-container-cluster" + ], "descriptiveName": "GCP Dns Managed Zone", "supportedQueryMethods": { "get": true, @@ -14,4 +17,4 @@ "terraformQueryMap": "google_dns_managed_zone.name" } ] -} +} \ No newline at end of file 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 index da7a9ff2..76dd3031 100644 --- 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 @@ -15,4 +15,4 @@ "terraformQueryMap": "google_essential_contacts_contact.id" } ] -} +} \ No newline at end of file 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 index 1a538cc3..5dfae89f 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-file-instance.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-file-instance.json @@ -1,7 +1,10 @@ { "type": "gcp-file-instance", "category": 2, - "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-compute-network"], + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-compute-network" + ], "descriptiveName": "GCP File Instance", "supportedQueryMethods": { "get": true, @@ -15,4 +18,4 @@ "terraformQueryMap": "google_filestore_instance.id" } ] -} +} \ No newline at end of file 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 index 1f4396c5..bf0ce0e2 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-role.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-role.json @@ -8,4 +8,4 @@ "list": true, "listDescription": "List all gcp-iam-role" } -} +} \ No newline at end of file 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 index 07a30c6a..48fcb53c 100644 --- 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 @@ -1,7 +1,9 @@ { "type": "gcp-iam-service-account-key", "category": 4, - "potentialLinks": ["gcp-iam-service-account"], + "potentialLinks": [ + "gcp-iam-service-account" + ], "descriptiveName": "GCP Iam Service Account Key", "supportedQueryMethods": { "get": true, @@ -11,7 +13,8 @@ }, "terraformMappings": [ { + "terraformMethod": 2, "terraformQueryMap": "google_service_account_key.id" } ] -} +} \ No newline at end of file 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 index f4e8078e..2f6f6f50 100644 --- 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 @@ -20,4 +20,4 @@ "terraformQueryMap": "google_service_account.unique_id" } ] -} +} \ No newline at end of file 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 index e63a32f8..75c77fc3 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-bucket.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-bucket.json @@ -1,7 +1,11 @@ { "type": "gcp-logging-bucket", "category": 5, - "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-iam-service-account"], + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-cloud-kms-crypto-key-version", + "gcp-iam-service-account" + ], "descriptiveName": "GCP Logging Bucket", "supportedQueryMethods": { "get": true, @@ -9,4 +13,4 @@ "search": true, "searchDescription": "Search for gcp-logging-bucket by its \"locations\"" } -} +} \ No newline at end of file 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 index 319a71a0..8895fca1 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-link.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-link.json @@ -1,7 +1,10 @@ { "type": "gcp-logging-link", "category": 5, - "potentialLinks": ["gcp-big-query-dataset", "gcp-logging-bucket"], + "potentialLinks": [ + "gcp-big-query-dataset", + "gcp-logging-bucket" + ], "descriptiveName": "GCP Logging Link", "supportedQueryMethods": { "get": true, @@ -9,4 +12,4 @@ "search": true, "searchDescription": "Search for gcp-logging-link by its \"locations|buckets\"" } -} +} \ No newline at end of file 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 index f68dd359..75db0a53 100644 --- 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 @@ -8,4 +8,4 @@ "search": true, "searchDescription": "Search for gcp-logging-saved-query by its \"locations\"" } -} +} \ No newline at end of file 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 index 013bcc3b..f80a43f5 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-sink.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-sink.json @@ -3,6 +3,7 @@ "category": 7, "potentialLinks": [ "gcp-big-query-dataset", + "gcp-iam-service-account", "gcp-logging-bucket", "gcp-pub-sub-topic", "gcp-storage-bucket" @@ -14,4 +15,4 @@ "list": true, "listDescription": "List all GCP Logging Sink items" } -} +} \ No newline at end of file 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 index 89297235..e3319196 100644 --- 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 @@ -1,7 +1,9 @@ { "type": "gcp-monitoring-alert-policy", "category": 5, - "potentialLinks": ["gcp-monitoring-notification-channel"], + "potentialLinks": [ + "gcp-monitoring-notification-channel" + ], "descriptiveName": "GCP Monitoring Alert Policy", "supportedQueryMethods": { "get": true, @@ -17,4 +19,4 @@ "terraformQueryMap": "google_monitoring_alert_policy.id" } ] -} +} \ No newline at end of file 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 index 5dcb1ef2..ed6ef2b3 100644 --- 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 @@ -16,4 +16,4 @@ "terraformQueryMap": "google_monitoring_dashboard.id" } ] -} +} \ No newline at end of file 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 index 9da3e095..0153d49b 100644 --- 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 @@ -1,6 +1,9 @@ { "type": "gcp-monitoring-notification-channel", "category": 5, + "potentialLinks": [ + "gcp-pub-sub-topic" + ], "descriptiveName": "GCP Monitoring Notification Channel", "supportedQueryMethods": { "get": true, @@ -15,4 +18,4 @@ "terraformQueryMap": "google_monitoring_notification_channel.name" } ] -} +} \ No newline at end of file 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 index 0126fcb8..a61c8d26 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-orgpolicy-policy.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-orgpolicy-policy.json @@ -1,6 +1,9 @@ { "type": "gcp-orgpolicy-policy", "category": 7, + "potentialLinks": [ + "gcp-cloud-resource-manager-project" + ], "descriptiveName": "GCP Orgpolicy Policy", "supportedQueryMethods": { "get": true, @@ -16,4 +19,4 @@ "terraformQueryMap": "google_org_policy_policy.name" } ] -} +} \ No newline at end of file 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 index 35abc174..39649d1e 100644 --- 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 @@ -3,6 +3,7 @@ "category": 7, "potentialLinks": [ "gcp-big-query-table", + "gcp-iam-service-account", "gcp-pub-sub-subscription", "gcp-pub-sub-topic", "gcp-storage-bucket" @@ -17,6 +18,15 @@ "terraformMappings": [ { "terraformQueryMap": "google_pubsub_subscription.name" + }, + { + "terraformQueryMap": "google_pubsub_subscription_iam_binding.subscription" + }, + { + "terraformQueryMap": "google_pubsub_subscription_iam_member.subscription" + }, + { + "terraformQueryMap": "google_pubsub_subscription_iam_policy.subscription" } ] -} +} \ No newline at end of file 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 index 7f8a34b0..467457e8 100644 --- 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 @@ -1,7 +1,11 @@ { "type": "gcp-pub-sub-topic", "category": 7, - "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-storage-bucket"], + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-iam-service-account", + "gcp-storage-bucket" + ], "descriptiveName": "GCP Pub Sub Topic", "supportedQueryMethods": { "get": true, @@ -12,6 +16,15 @@ "terraformMappings": [ { "terraformQueryMap": "google_pubsub_topic.name" + }, + { + "terraformQueryMap": "google_pubsub_topic_iam_binding.topic" + }, + { + "terraformQueryMap": "google_pubsub_topic_iam_member.topic" + }, + { + "terraformQueryMap": "google_pubsub_topic_iam_policy.topic" } ] -} +} \ No newline at end of file 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 index 96b1fcd6..ffcf3fdf 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-redis-instance.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-redis-instance.json @@ -19,4 +19,4 @@ "terraformQueryMap": "google_redis_instance.id" } ] -} +} \ No newline at end of file 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 index 44d783b7..6768f584 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-run-revision.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-run-revision.json @@ -8,6 +8,7 @@ "gcp-compute-subnetwork", "gcp-iam-service-account", "gcp-run-service", + "gcp-secret-manager-secret", "gcp-sql-admin-instance", "gcp-storage-bucket" ], @@ -18,4 +19,4 @@ "search": true, "searchDescription": "Search for gcp-run-revision by its \"locations|services\"" } -} +} \ No newline at end of file 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 index 74851f26..9bdb3abb 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-run-service.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-run-service.json @@ -25,4 +25,4 @@ "terraformQueryMap": "google_cloud_run_v2_service.id" } ] -} +} \ No newline at end of file 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 index c85bf1d8..e169de67 100644 --- 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 @@ -1,7 +1,10 @@ { "type": "gcp-secret-manager-secret", "category": 4, - "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-pub-sub-topic"], + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-pub-sub-topic" + ], "descriptiveName": "GCP Secret Manager Secret", "supportedQueryMethods": { "get": true, @@ -14,4 +17,4 @@ "terraformQueryMap": "google_secret_manager_secret.secret_id" } ] -} +} \ No newline at end of file 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 index f5c6408f..5496fff4 100644 --- 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 @@ -1,6 +1,9 @@ { "type": "gcp-security-center-management-security-center-service", "category": 4, + "potentialLinks": [ + "gcp-cloud-resource-manager-project" + ], "descriptiveName": "GCP Security Center Management Security Center Service", "supportedQueryMethods": { "get": true, @@ -8,4 +11,4 @@ "search": true, "searchDescription": "Search Security Center services in a location. Use the format \"location\"." } -} +} \ No newline at end of file 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 index 83acd69d..ec76f68e 100644 --- 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 @@ -1,7 +1,9 @@ { "type": "gcp-service-directory-endpoint", "category": 7, - "potentialLinks": ["gcp-compute-network"], + "potentialLinks": [ + "gcp-compute-network" + ], "descriptiveName": "GCP Service Directory Endpoint", "supportedQueryMethods": { "get": true, @@ -15,4 +17,4 @@ "terraformQueryMap": "google_service_directory_endpoint.id" } ] -} +} \ No newline at end of file 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 index 8c15f219..53fa6f74 100644 --- 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 @@ -1,7 +1,10 @@ { "type": "gcp-service-usage-service", "category": 7, - "potentialLinks": ["gcp-pub-sub-topic"], + "potentialLinks": [ + "gcp-cloud-resource-manager-project", + "gcp-pub-sub-topic" + ], "descriptiveName": "GCP Service Usage Service", "supportedQueryMethods": { "get": true, @@ -9,4 +12,4 @@ "list": true, "listDescription": "List all gcp-service-usage-service" } -} +} \ No newline at end of file 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 index f333a2dc..27b3c0ad 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-database.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-database.json @@ -1,7 +1,12 @@ { "type": "gcp-spanner-database", "category": 6, - "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-spanner-instance"], + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-cloud-kms-crypto-key-version", + "gcp-spanner-database", + "gcp-spanner-instance" + ], "descriptiveName": "GCP Spanner Database", "supportedQueryMethods": { "get": true, @@ -14,4 +19,4 @@ "terraformQueryMap": "google_spanner_database.name" } ] -} +} \ No newline at end of file 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 index b3193800..e44cc5d5 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-instance.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-instance.json @@ -1,7 +1,9 @@ { "type": "gcp-spanner-instance", "category": 6, - "potentialLinks": ["gcp-spanner-database"], + "potentialLinks": [ + "gcp-spanner-database" + ], "descriptiveName": "GCP Spanner Instance", "supportedQueryMethods": { "get": true, @@ -14,4 +16,4 @@ "terraformQueryMap": "google_spanner_instance.name" } ] -} +} \ No newline at end of file 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 index 1071747e..f4a27d72 100644 --- 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 @@ -1,7 +1,11 @@ { "type": "gcp-sql-admin-backup-run", "category": 6, - "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-sql-admin-instance"], + "potentialLinks": [ + "gcp-cloud-kms-crypto-key", + "gcp-cloud-kms-crypto-key-version", + "gcp-sql-admin-instance" + ], "descriptiveName": "GCP Sql Admin Backup Run", "supportedQueryMethods": { "get": true, @@ -9,4 +13,4 @@ "search": true, "searchDescription": "Search for gcp-sql-admin-backup-run by its \"instances\"" } -} +} \ No newline at end of file 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 index 474f8076..ced944f2 100644 --- 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 @@ -3,7 +3,8 @@ "category": 6, "potentialLinks": [ "gcp-cloud-kms-crypto-key", - "gcp-sql-admin-backup-run", + "gcp-cloud-kms-crypto-key-version", + "gcp-compute-network", "gcp-sql-admin-instance" ], "descriptiveName": "GCP Sql Admin Backup", @@ -13,4 +14,4 @@ "list": true, "listDescription": "List all gcp-sql-admin-backup" } -} +} \ No newline at end of file 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 index 16d72392..b19914bf 100644 --- 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 @@ -4,6 +4,7 @@ "potentialLinks": [ "gcp-cloud-kms-crypto-key", "gcp-compute-network", + "gcp-compute-subnetwork", "gcp-iam-service-account", "gcp-sql-admin-backup-run", "gcp-sql-admin-instance", @@ -21,4 +22,4 @@ "terraformQueryMap": "google_sql_database_instance.name" } ] -} +} \ No newline at end of file diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket-iam-policy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket-iam-policy.json new file mode 100644 index 00000000..350a3274 --- /dev/null +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket-iam-policy.json @@ -0,0 +1,28 @@ +{ + "type": "gcp-storage-bucket-iam-policy", + "category": 4, + "potentialLinks": [ + "gcp-compute-project", + "gcp-iam-role", + "gcp-iam-service-account", + "gcp-storage-bucket" + ], + "descriptiveName": "GCP Storage Bucket Iam Policy", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get GCP Storage Bucket Iam Policy by \"gcp-storage-bucket-iam-policy-bucket\"", + "search": true, + "searchDescription": "Search for GCP Storage Bucket Iam Policy by \"gcp-storage-bucket-iam-policy-bucket\"" + }, + "terraformMappings": [ + { + "terraformQueryMap": "google_storage_bucket_iam_binding.bucket" + }, + { + "terraformQueryMap": "google_storage_bucket_iam_member.bucket" + }, + { + "terraformQueryMap": "google_storage_bucket_iam_policy.bucket" + } + ] +} \ No newline at end of file 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 index 95730649..57b9d8e3 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket.json @@ -4,7 +4,8 @@ "potentialLinks": [ "gcp-cloud-kms-crypto-key", "gcp-compute-network", - "gcp-logging-bucket" + "gcp-logging-bucket", + "gcp-storage-bucket-iam-policy" ], "descriptiveName": "GCP Storage Bucket", "supportedQueryMethods": { @@ -16,6 +17,15 @@ "terraformMappings": [ { "terraformQueryMap": "google_storage_bucket.name" + }, + { + "terraformQueryMap": "google_storage_bucket_iam_binding.bucket" + }, + { + "terraformQueryMap": "google_storage_bucket_iam_member.bucket" + }, + { + "terraformQueryMap": "google_storage_bucket_iam_policy.bucket" } ] -} +} \ No newline at end of file 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 index 2b3bf896..54242757 100644 --- 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 @@ -5,6 +5,7 @@ "gcp-iam-service-account", "gcp-pub-sub-subscription", "gcp-pub-sub-topic", + "gcp-secret-manager-secret", "gcp-storage-bucket" ], "descriptiveName": "GCP Storage Transfer Transfer Job", @@ -19,4 +20,4 @@ "terraformQueryMap": "google_storage_transfer_job.name" } ] -} +} \ No newline at end of file diff --git a/sources/gcp/dynamic/adapters/compute-firewall.go b/sources/gcp/dynamic/adapters/compute-firewall.go index 02b70c11..d3b2d11d 100644 --- a/sources/gcp/dynamic/adapters/compute-firewall.go +++ b/sources/gcp/dynamic/adapters/compute-firewall.go @@ -40,11 +40,11 @@ var _ = registerableAdapter{ "sourceServiceAccounts": gcpshared.IAMServiceAccountImpactInOnly, "targetServiceAccounts": gcpshared.IAMServiceAccountImpactInOnly, "targetTags": { - Description: "Firewall targets instances with this network tag. Changing the tag on either side affects which VMs the rule applies to.", + Description: "Firewall rule specifies target_tags to control traffic to VM instances and instance templates with those tags. Overmind automatically discovers these relationships by searching for instances and templates with matching network tags, enabling accurate blast radius analysis when tags change on either firewalls or instances.", ToSDPItemType: gcpshared.ComputeInstance, }, "sourceTags": { - Description: "Firewall allows traffic from instances with this network tag. Changing the tag on either side affects traffic flow.", + Description: "Firewall rule specifies source_tags to control traffic from VM instances with those tags. Overmind automatically discovers these relationships by searching for instances with matching network tags, enabling accurate blast radius analysis when tags change on either firewalls or instances.", ToSDPItemType: gcpshared.ComputeInstance, }, }, diff --git a/sources/gcp/dynamic/adapters/compute-instance-template.go b/sources/gcp/dynamic/adapters/compute-instance-template.go index 0743d00f..a5b9b7b4 100644 --- a/sources/gcp/dynamic/adapters/compute-instance-template.go +++ b/sources/gcp/dynamic/adapters/compute-instance-template.go @@ -1,6 +1,9 @@ package adapters import ( + "fmt" + "strings" + "github.com/overmindtech/cli/go/sdp-go" gcpshared "github.com/overmindtech/cli/sources/gcp/shared" "github.com/overmindtech/cli/sources/stdlib" @@ -19,6 +22,15 @@ var _ = registerableAdapter{ UniqueAttributeKeys: []string{"instanceTemplates"}, IAMPermissions: []string{"compute.instanceTemplates.get", "compute.instanceTemplates.list"}, PredefinedRole: "roles/compute.viewer", + // Tag-based SEARCH: list all instance templates then filter by tag. + SearchEndpointFunc: func(query string, location gcpshared.LocationInfo) string { + if query == "" || strings.Contains(query, "/") { + return "" + } + return fmt.Sprintf("https://compute.googleapis.com/compute/v1/projects/%s/global/instanceTemplates", location.ProjectID) + }, + SearchDescription: "Search for instance templates by network tag. The query is a plain network tag name.", + SearchFilterFunc: instanceTemplateTagFilter, }, linkRules: map[string]*gcpshared.Impact{ // https://cloud.google.com/compute/docs/reference/rest/v1/instanceTemplates/get @@ -109,7 +121,7 @@ var _ = registerableAdapter{ ToSDPItemType: gcpshared.IAMServiceAccount, }, "properties.tags.items": { - Description: "Network tag on the instance template. Changing this tag affects which firewall rules and routes apply to instances created from this template.", + Description: "Instance templates define network tags that will be applied to instances created from the template. Overmind discovers firewall rules and routes with matching tags, showing how firewall and route changes will affect instances created from this template.", ToSDPItemType: gcpshared.ComputeFirewall, }, }, @@ -123,3 +135,8 @@ var _ = registerableAdapter{ }, }, }.Register() + +// instanceTemplateTagFilter keeps instance templates whose properties.tags.items contain the query tag. +func instanceTemplateTagFilter(query string, item *sdp.Item) bool { + return itemAttributeContainsTag(item, "properties.tags.items", query) +} diff --git a/sources/gcp/dynamic/adapters/compute-route.go b/sources/gcp/dynamic/adapters/compute-route.go index 2d9206b9..e39dfe5f 100644 --- a/sources/gcp/dynamic/adapters/compute-route.go +++ b/sources/gcp/dynamic/adapters/compute-route.go @@ -78,7 +78,7 @@ var _ = registerableAdapter{ ToSDPItemType: gcpshared.ComputeInterconnectAttachment, }, "tags": { - Description: "Route applies to instances with this network tag. Changing the tag on either side affects which VMs the route targets.", + Description: "Route specifies network tags to apply routing rules only to instances and instance templates with matching tags. Overmind automatically discovers instances and templates with these tags, enabling blast radius analysis to show which resources will be affected when you modify a route's tags.", ToSDPItemType: gcpshared.ComputeInstance, }, }, diff --git a/sources/gcp/integration-tests/network-tags_test.go b/sources/gcp/integration-tests/network-tags_test.go index f14af807..634cb3f4 100644 --- a/sources/gcp/integration-tests/network-tags_test.go +++ b/sources/gcp/integration-tests/network-tags_test.go @@ -2,7 +2,7 @@ // // All: go test ./sources/gcp/integration-tests/ -run "TestNetworkTagRelationships" -count 1 -v // Setup: go test ./sources/gcp/integration-tests/ -run "TestNetworkTagRelationships/Setup" -count 1 -v -// Run: go test ./sources/gcp/integration-tests/ -run "TestNetworkTagRelationships/(Test|[A-Z])" -count 1 -v +// Run: go test ./sources/gcp/integration-tests/ -run "TestNetworkTagRelationships/(Instance|Firewall|Route)" -count 1 -v // Teardown: go test ./sources/gcp/integration-tests/ -run "TestNetworkTagRelationships/Teardown" -count 1 -v // // Verify created resources with gcloud: @@ -10,6 +10,7 @@ // gcloud compute instances describe integration-test-nettag-instance --zone=$GCP_ZONE --project=$GCP_PROJECT_ID --format="value(tags.items)" // gcloud compute firewall-rules describe integration-test-nettag-fw --project=$GCP_PROJECT_ID --format="value(targetTags)" // gcloud compute routes describe integration-test-nettag-route --project=$GCP_PROJECT_ID --format="value(tags)" +// package integrationtests @@ -25,7 +26,6 @@ import ( "cloud.google.com/go/compute/apiv1/computepb" "github.com/googleapis/gax-go/v2/apierror" log "github.com/sirupsen/logrus" - "k8s.io/utils/ptr" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" @@ -37,10 +37,11 @@ import ( ) const ( - networkTagTestInstance = "integration-test-nettag-instance" - networkTagTestFirewall = "integration-test-nettag-fw" - networkTagTestRoute = "integration-test-nettag-route" - networkTag = "nettag-test" + networkTagTestInstance = "integration-test-nettag-instance" + networkTagTestFirewall = "integration-test-nettag-fw" + networkTagTestRoute = "integration-test-nettag-route" + networkTagTestInstanceTemplate = "integration-test-nettag-template" + networkTag = "nettag-test" ) func TestNetworkTagRelationships(t *testing.T) { @@ -76,6 +77,12 @@ func TestNetworkTagRelationships(t *testing.T) { } defer routeClient.Close() + instanceTemplateClient, err := compute.NewInstanceTemplatesRESTClient(ctx) + if err != nil { + t.Fatalf("NewInstanceTemplatesRESTClient: %v", err) + } + defer instanceTemplateClient.Close() + // --- Setup --- t.Run("Setup", func(t *testing.T) { if err := createInstanceWithTags(ctx, instanceClient, projectID, zone); err != nil { @@ -87,6 +94,9 @@ func TestNetworkTagRelationships(t *testing.T) { if err := createRouteWithTags(ctx, routeClient, projectID); err != nil { t.Fatalf("Failed to create tagged route: %v", err) } + if err := createInstanceTemplateWithTags(ctx, instanceTemplateClient, projectID); err != nil { + t.Fatalf("Failed to create tagged instance template: %v", err) + } }) // --- Run --- @@ -172,6 +182,36 @@ func TestNetworkTagRelationships(t *testing.T) { } }) + t.Run("InstanceSearchByTagReturnsInstance", func(t *testing.T) { + wrapper := manual.NewComputeInstance( + gcpshared.NewComputeInstanceClient(instanceClient), + []gcpshared.LocationInfo{gcpshared.NewZonalLocation(projectID, zone)}, + ) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Fatalf("Instance adapter does not implement SearchableAdapter") + } + + scopeWithZone := fmt.Sprintf("%s.%s", projectID, zone) + items, qErr := searchable.Search(ctx, scopeWithZone, networkTag, true) + if qErr != nil { + t.Fatalf("Search: %v", qErr) + } + + found := false + for _, item := range items { + if v, err := item.GetAttributes().Get("name"); err == nil && v == networkTagTestInstance { + found = true + break + } + } + if !found { + t.Errorf("Expected to find instance %s in search results for tag %q, got %d items", networkTagTestInstance, networkTag, len(items)) + } + }) + t.Run("FirewallEmitsSearchLinksToInstance", func(t *testing.T) { gcpHTTPCli, err := gcpshared.GCPHTTPClientWithOtel(ctx, "") if err != nil { @@ -191,6 +231,45 @@ func TestNetworkTagRelationships(t *testing.T) { assertHasLinkedItemQuery(t, sdpItem, gcpshared.ComputeInstance.String(), sdp.QueryMethod_SEARCH, networkTag, projectID) }) + t.Run("RouteEmitsSearchLinksToInstance", func(t *testing.T) { + gcpHTTPCli, err := gcpshared.GCPHTTPClientWithOtel(ctx, "") + if err != nil { + t.Fatalf("GCPHTTPClientWithOtel: %v", err) + } + + adapter, err := dynamic.MakeAdapter(gcpshared.ComputeRoute, gcpshared.NewLinker(), gcpHTTPCli, sdpcache.NewNoOpCache(), []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + if err != nil { + t.Fatalf("MakeAdapter: %v", err) + } + + sdpItem, qErr := adapter.Get(ctx, projectID, networkTagTestRoute, true) + if qErr != nil { + t.Fatalf("Get route: %v", qErr) + } + + assertHasLinkedItemQuery(t, sdpItem, gcpshared.ComputeInstance.String(), sdp.QueryMethod_SEARCH, networkTag, projectID) + }) + + t.Run("InstanceTemplateEmitsSearchLinksToFirewallAndRoute", func(t *testing.T) { + gcpHTTPCli, err := gcpshared.GCPHTTPClientWithOtel(ctx, "") + if err != nil { + t.Fatalf("GCPHTTPClientWithOtel: %v", err) + } + + adapter, err := dynamic.MakeAdapter(gcpshared.ComputeInstanceTemplate, gcpshared.NewLinker(), gcpHTTPCli, sdpcache.NewNoOpCache(), []gcpshared.LocationInfo{gcpshared.NewProjectLocation(projectID)}) + if err != nil { + t.Fatalf("MakeAdapter: %v", err) + } + + sdpItem, qErr := adapter.Get(ctx, projectID, networkTagTestInstanceTemplate, true) + if qErr != nil { + t.Fatalf("Get instance template: %v", qErr) + } + + assertHasLinkedItemQuery(t, sdpItem, gcpshared.ComputeFirewall.String(), sdp.QueryMethod_SEARCH, networkTag, projectID) + assertHasLinkedItemQuery(t, sdpItem, gcpshared.ComputeRoute.String(), sdp.QueryMethod_SEARCH, networkTag, projectID) + }) + // --- Teardown --- t.Run("Teardown", func(t *testing.T) { if err := deleteComputeInstance(ctx, instanceClient, projectID, zone, networkTagTestInstance); err != nil { @@ -202,6 +281,9 @@ func TestNetworkTagRelationships(t *testing.T) { if err := deleteRoute(ctx, routeClient, projectID, networkTagTestRoute); err != nil { t.Errorf("Failed to delete route: %v", err) } + if err := deleteInstanceTemplate(ctx, instanceTemplateClient, projectID, networkTagTestInstanceTemplate); err != nil { + t.Errorf("Failed to delete instance template: %v", err) + } }) } @@ -221,23 +303,23 @@ func assertHasLinkedItemQuery(t *testing.T, item *sdp.Item, expectedType string, func createInstanceWithTags(ctx context.Context, client *compute.InstancesClient, projectID, zone string) error { instance := &computepb.Instance{ - Name: ptr.To(networkTagTestInstance), - MachineType: ptr.To(fmt.Sprintf("zones/%s/machineTypes/e2-micro", zone)), + Name: new(networkTagTestInstance), + MachineType: new(fmt.Sprintf("zones/%s/machineTypes/e2-micro", zone)), Tags: &computepb.Tags{ Items: []string{networkTag}, }, Disks: []*computepb.AttachedDisk{ { - Boot: ptr.To(true), - AutoDelete: ptr.To(true), + Boot: new(true), + AutoDelete: new(true), InitializeParams: &computepb.AttachedDiskInitializeParams{ - SourceImage: ptr.To("projects/debian-cloud/global/images/debian-12-bookworm-v20250415"), - DiskSizeGb: ptr.To(int64(10)), + SourceImage: new("projects/debian-cloud/global/images/debian-12-bookworm-v20250415"), + DiskSizeGb: new(int64(10)), }, }, }, NetworkInterfaces: []*computepb.NetworkInterface{ - {StackType: ptr.To("IPV4_ONLY")}, + {StackType: new("IPV4_ONLY")}, }, } @@ -259,12 +341,12 @@ func createInstanceWithTags(ctx context.Context, client *compute.InstancesClient func createFirewallWithTags(ctx context.Context, client *compute.FirewallsClient, projectID string) error { fw := &computepb.Firewall{ - Name: ptr.To(networkTagTestFirewall), - Network: ptr.To(fmt.Sprintf("projects/%s/global/networks/default", projectID)), + Name: new(networkTagTestFirewall), + Network: new(fmt.Sprintf("projects/%s/global/networks/default", projectID)), TargetTags: []string{networkTag}, Allowed: []*computepb.Allowed{ { - IPProtocol: ptr.To("tcp"), + IPProtocol: new("tcp"), Ports: []string{"8080"}, }, }, @@ -288,12 +370,12 @@ func createFirewallWithTags(ctx context.Context, client *compute.FirewallsClient func createRouteWithTags(ctx context.Context, client *compute.RoutesClient, projectID string) error { route := &computepb.Route{ - Name: ptr.To(networkTagTestRoute), - Network: ptr.To(fmt.Sprintf("projects/%s/global/networks/default", projectID)), - DestRange: ptr.To("10.99.0.0/24"), - NextHopGateway: ptr.To(fmt.Sprintf("projects/%s/global/gateways/default-internet-gateway", projectID)), + Name: new(networkTagTestRoute), + Network: new(fmt.Sprintf("projects/%s/global/networks/default", projectID)), + DestRange: new("10.99.0.0/24"), + NextHopGateway: new(fmt.Sprintf("projects/%s/global/gateways/default-internet-gateway", projectID)), Tags: []string{networkTag}, - Priority: ptr.To(uint32(900)), + Priority: new(uint32(900)), } op, err := client.Insert(ctx, &computepb.InsertRouteRequest{ @@ -340,3 +422,45 @@ func deleteRoute(ctx context.Context, client *compute.RoutesClient, projectID, n } return op.Wait(ctx) } + +func createInstanceTemplateWithTags(ctx context.Context, client *compute.InstanceTemplatesClient, projectID string) error { + template := &computepb.InstanceTemplate{ + Name: new(networkTagTestInstanceTemplate), + Properties: &computepb.InstanceProperties{ + MachineType: new("e2-micro"), + Tags: &computepb.Tags{ + Items: []string{networkTag}, + }, + Disks: []*computepb.AttachedDisk{ + { + Boot: new(true), + AutoDelete: new(true), + InitializeParams: &computepb.AttachedDiskInitializeParams{ + SourceImage: new("projects/debian-cloud/global/images/debian-12-bookworm-v20250415"), + DiskSizeGb: new(int64(10)), + }, + }, + }, + NetworkInterfaces: []*computepb.NetworkInterface{ + { + Network: new("global/networks/default"), + StackType: new("IPV4_ONLY"), + }, + }, + }, + } + + op, err := client.Insert(ctx, &computepb.InsertInstanceTemplateRequest{ + Project: projectID, + InstanceTemplateResource: template, + }) + if err != nil { + var apiErr *apierror.APIError + if errors.As(err, &apiErr) && apiErr.HTTPCode() == http.StatusConflict { + log.Printf("Instance template %s already exists, skipping", networkTagTestInstanceTemplate) + return nil + } + return fmt.Errorf("insert instance template: %w", err) + } + return op.Wait(ctx) +} diff --git a/sources/gcp/shared/linker.go b/sources/gcp/shared/linker.go index 8881884f..47872c12 100644 --- a/sources/gcp/shared/linker.go +++ b/sources/gcp/shared/linker.go @@ -91,16 +91,26 @@ func (l *Linker) AutoLink(ctx context.Context, projectID string, fromSDPItem *sd switch fromSDPItemType { case ComputeFirewall, ComputeRoute: - // Tag-based SEARCH lists all instances in scope then filters; + // Tag-based SEARCH lists all instances and instance templates in scope then filters; // may be slow in very large projects. - fromSDPItem.LinkedItemQueries = append(fromSDPItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: ComputeInstance.String(), - Method: sdp.QueryMethod_SEARCH, - Query: tag, - Scope: projectID, + fromSDPItem.LinkedItemQueries = append(fromSDPItem.LinkedItemQueries, + &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: ComputeInstance.String(), + Method: sdp.QueryMethod_SEARCH, + Query: tag, + Scope: projectID, + }, }, - }) + &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: ComputeInstanceTemplate.String(), + Method: sdp.QueryMethod_SEARCH, + Query: tag, + Scope: projectID, + }, + }, + ) case ComputeInstance, ComputeInstanceTemplate: // Tag-based SEARCH lists all firewalls/routes in scope then filters; // may be slow in very large projects. From e07a55233aa4cb3de03e6df6b9287e1ae67cacdc Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:58:29 +0000 Subject: [PATCH 21/74] Add Azure SQL Server Virtual Network Rule Client and Adapter (#4024) image > [!NOTE] > **Low Risk** > Mostly additive wiring for a new read-only Azure resource type; main risk is miswiring the new adapter/client causing discovery/runtime query failures or extra Azure API usage. > > **Overview** > Adds first-class discovery support for Azure SQL Server Virtual Network Rules via a new `SqlServerVirtualNetworkRuleClient` and a `NewSqlServerVirtualNetworkRule` manual adapter implementing `Get`, `Search`, and `SearchStream`. > > Wires the new adapter into Azure `Adapters()` initialization (including a new `armsql.NewVirtualNetworkRulesClient`) and into metadata-only adapter enumeration, and updates Azure resource-id path key mapping for `azure-sql-server-virtual-network-rule`. Includes generated GoMock and unit tests covering happy paths, subnet/vnet linking, and error handling. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ac3a7f2c9dcf2c857f1e2e94b1a6d6227aa05b62. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 092b75194a05270320d39f68938f2f9fa7b3b53d --- .../sql-server-virtual-network-rule-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + .../manual/sql-server-virtual-network-rule.go | 260 +++++++++++++ .../sql-server-virtual-network-rule_test.go | 359 ++++++++++++++++++ ..._sql_server_virtual_network_rule_client.go | 72 ++++ sources/azure/shared/utils.go | 1 + 6 files changed, 737 insertions(+) create mode 100644 sources/azure/clients/sql-server-virtual-network-rule-client.go create mode 100644 sources/azure/manual/sql-server-virtual-network-rule.go create mode 100644 sources/azure/manual/sql-server-virtual-network-rule_test.go create mode 100644 sources/azure/shared/mocks/mock_sql_server_virtual_network_rule_client.go diff --git a/sources/azure/clients/sql-server-virtual-network-rule-client.go b/sources/azure/clients/sql-server-virtual-network-rule-client.go new file mode 100644 index 00000000..71ffb2ec --- /dev/null +++ b/sources/azure/clients/sql-server-virtual-network-rule-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" +) + +//go:generate mockgen -destination=../shared/mocks/mock_sql_server_virtual_network_rule_client.go -package=mocks -source=sql-server-virtual-network-rule-client.go + +// SqlServerVirtualNetworkRulePager is a type alias for the generic Pager interface with SQL server virtual network rule list response type. +type SqlServerVirtualNetworkRulePager = Pager[armsql.VirtualNetworkRulesClientListByServerResponse] + +// SqlServerVirtualNetworkRuleClient is an interface for interacting with Azure SQL server virtual network rules. +type SqlServerVirtualNetworkRuleClient interface { + ListByServer(ctx context.Context, resourceGroupName string, serverName string) SqlServerVirtualNetworkRulePager + Get(ctx context.Context, resourceGroupName string, serverName string, virtualNetworkRuleName string) (armsql.VirtualNetworkRulesClientGetResponse, error) +} + +type sqlServerVirtualNetworkRuleClient struct { + client *armsql.VirtualNetworkRulesClient +} + +func (a *sqlServerVirtualNetworkRuleClient) ListByServer(ctx context.Context, resourceGroupName string, serverName string) SqlServerVirtualNetworkRulePager { + return a.client.NewListByServerPager(resourceGroupName, serverName, nil) +} + +func (a *sqlServerVirtualNetworkRuleClient) Get(ctx context.Context, resourceGroupName string, serverName string, virtualNetworkRuleName string) (armsql.VirtualNetworkRulesClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, serverName, virtualNetworkRuleName, nil) +} + +// NewSqlServerVirtualNetworkRuleClient creates a new SqlServerVirtualNetworkRuleClient from the Azure SDK client. +func NewSqlServerVirtualNetworkRuleClient(client *armsql.VirtualNetworkRulesClient) SqlServerVirtualNetworkRuleClient { + return &sqlServerVirtualNetworkRuleClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index d18a30b1..6a5dc1ce 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -225,6 +225,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create sql firewall rules client: %w", err) } + sqlVirtualNetworkRulesClient, err := armsql.NewVirtualNetworkRulesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create sql virtual network rules client: %w", err) + } + postgresqlFlexibleServersClient, err := armpostgresqlflexibleservers.NewServersClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create postgresql flexible servers client: %w", err) @@ -373,6 +378,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewSqlServerFirewallRuleClient(sqlFirewallRulesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewSqlServerVirtualNetworkRule( + clients.NewSqlServerVirtualNetworkRuleClient(sqlVirtualNetworkRulesClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewDocumentDBDatabaseAccounts( clients.NewDocumentDBDatabaseAccountsClient(documentDBDatabaseAccountsClient), resourceGroupScopes, @@ -549,6 +558,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewNetworkNetworkInterface(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewSqlDatabase(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewSqlServerFirewallRule(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewSqlServerVirtualNetworkRule(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDocumentDBDatabaseAccounts(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewKeyVaultVault(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewKeyVaultManagedHSM(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/sql-server-virtual-network-rule.go b/sources/azure/manual/sql-server-virtual-network-rule.go new file mode 100644 index 00000000..0a56dbee --- /dev/null +++ b/sources/azure/manual/sql-server-virtual-network-rule.go @@ -0,0 +1,260 @@ +package manual + +import ( + "context" + "errors" + "fmt" + + "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" + "github.com/overmindtech/cli/sources/shared" +) + +var SQLServerVirtualNetworkRuleLookupByName = shared.NewItemTypeLookup("name", azureshared.SQLServerVirtualNetworkRule) + +type sqlServerVirtualNetworkRuleWrapper struct { + client clients.SqlServerVirtualNetworkRuleClient + + *azureshared.MultiResourceGroupBase +} + +func NewSqlServerVirtualNetworkRule(client clients.SqlServerVirtualNetworkRuleClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &sqlServerVirtualNetworkRuleWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_DATABASE, + azureshared.SQLServerVirtualNetworkRule, + ), + } +} + +func (s sqlServerVirtualNetworkRuleWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: serverName and virtualNetworkRuleName", + Scope: scope, + ItemType: s.Type(), + } + } + serverName := queryParts[0] + ruleName := queryParts[1] + if ruleName == "" { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "virtualNetworkRuleName cannot be empty", + Scope: scope, + ItemType: s.Type(), + } + } + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + resp, err := s.client.Get(ctx, rgScope.ResourceGroup, serverName, ruleName) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + return s.azureSqlServerVirtualNetworkRuleToSDPItem(&resp.VirtualNetworkRule, serverName, ruleName, scope) +} + +func (s sqlServerVirtualNetworkRuleWrapper) azureSqlServerVirtualNetworkRuleToSDPItem(rule *armsql.VirtualNetworkRule, serverName, ruleName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(rule, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(serverName, ruleName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.SQLServerVirtualNetworkRule.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: nil, // VirtualNetworkRule has no Tags in the Azure SDK + } + + // Link to parent SQL Server (from resource ID or known server name) + if rule.ID != nil { + extractedServerName := azureshared.ExtractSQLServerNameFromDatabaseID(*rule.ID) + if extractedServerName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.SQLServer.String(), + Method: sdp.QueryMethod_GET, + Query: extractedServerName, + Scope: scope, + }, + }) + } + } else { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.SQLServer.String(), + Method: sdp.QueryMethod_GET, + Query: serverName, + Scope: scope, + }, + }) + } + + // Link to Virtual Network and Subnet when VirtualNetworkSubnetID is set + // Subnet ID format: /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName} + if rule.Properties != nil && rule.Properties.VirtualNetworkSubnetID != nil { + subnetID := *rule.Properties.VirtualNetworkSubnetID + scopeParams := azureshared.ExtractPathParamsFromResourceID(subnetID, []string{"subscriptions", "resourceGroups"}) + subnetParams := azureshared.ExtractPathParamsFromResourceID(subnetID, []string{"virtualNetworks", "subnets"}) + if len(scopeParams) >= 2 && len(subnetParams) >= 2 { + subscriptionID := scopeParams[0] + resourceGroupName := scopeParams[1] + vnetName := subnetParams[0] + subnetName := subnetParams[1] + subnetScope := fmt.Sprintf("%s.%s", subscriptionID, resourceGroupName) + // Link to Virtual Network + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkVirtualNetwork.String(), + Method: sdp.QueryMethod_GET, + Query: vnetName, + Scope: subnetScope, + }, + }) + // Link to Subnet + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkSubnet.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(vnetName, subnetName), + Scope: subnetScope, + }, + }) + } + } + + return sdpItem, nil +} + +func (s sqlServerVirtualNetworkRuleWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + SQLServerLookupByName, + SQLServerVirtualNetworkRuleLookupByName, + } +} + +func (s sqlServerVirtualNetworkRuleWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: serverName", + Scope: scope, + ItemType: s.Type(), + } + } + serverName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + pager := s.client.ListByServer(ctx, rgScope.ResourceGroup, serverName) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + for _, rule := range page.Value { + if rule.Name == nil { + continue + } + item, sdpErr := s.azureSqlServerVirtualNetworkRuleToSDPItem(rule, serverName, *rule.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (s sqlServerVirtualNetworkRuleWrapper) 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 _, rule := range page.Value { + if rule.Name == nil { + continue + } + item, sdpErr := s.azureSqlServerVirtualNetworkRuleToSDPItem(rule, serverName, *rule.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (s sqlServerVirtualNetworkRuleWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + SQLServerLookupByName, + }, + } +} + +func (s sqlServerVirtualNetworkRuleWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.SQLServer: true, + azureshared.NetworkSubnet: true, + azureshared.NetworkVirtualNetwork: true, + } +} + +func (s sqlServerVirtualNetworkRuleWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_mssql_virtual_network_rule.id", + }, + } +} + +func (s sqlServerVirtualNetworkRuleWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Sql/servers/virtualNetworkRules/read", + } +} + +func (s sqlServerVirtualNetworkRuleWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/sql-server-virtual-network-rule_test.go b/sources/azure/manual/sql-server-virtual-network-rule_test.go new file mode 100644 index 00000000..9182f287 --- /dev/null +++ b/sources/azure/manual/sql-server-virtual-network-rule_test.go @@ -0,0 +1,359 @@ +package manual_test + +import ( + "context" + "errors" + "slices" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" + "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" +) + +type mockSqlServerVirtualNetworkRulePager struct { + pages []armsql.VirtualNetworkRulesClientListByServerResponse + index int +} + +func (m *mockSqlServerVirtualNetworkRulePager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockSqlServerVirtualNetworkRulePager) NextPage(ctx context.Context) (armsql.VirtualNetworkRulesClientListByServerResponse, error) { + if m.index >= len(m.pages) { + return armsql.VirtualNetworkRulesClientListByServerResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type errorSqlServerVirtualNetworkRulePager struct{} + +func (e *errorSqlServerVirtualNetworkRulePager) More() bool { + return true +} + +func (e *errorSqlServerVirtualNetworkRulePager) NextPage(ctx context.Context) (armsql.VirtualNetworkRulesClientListByServerResponse, error) { + return armsql.VirtualNetworkRulesClientListByServerResponse{}, errors.New("pager error") +} + +type testSqlServerVirtualNetworkRuleClient struct { + *mocks.MockSqlServerVirtualNetworkRuleClient + pager clients.SqlServerVirtualNetworkRulePager +} + +func (t *testSqlServerVirtualNetworkRuleClient) ListByServer(ctx context.Context, resourceGroupName, serverName string) clients.SqlServerVirtualNetworkRulePager { + return t.pager +} + +func TestSqlServerVirtualNetworkRule(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + serverName := "test-server" + ruleName := "test-vnet-rule" + + t.Run("Get", func(t *testing.T) { + rule := createAzureSqlServerVirtualNetworkRule(serverName, ruleName, "") + + mockClient := mocks.NewMockSqlServerVirtualNetworkRuleClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, ruleName).Return( + armsql.VirtualNetworkRulesClientGetResponse{ + VirtualNetworkRule: *rule, + }, nil) + + wrapper := manual.NewSqlServerVirtualNetworkRule(&testSqlServerVirtualNetworkRuleClient{MockSqlServerVirtualNetworkRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, ruleName) + 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.SQLServerVirtualNetworkRule.String() { + t.Errorf("Expected type %s, got %s", azureshared.SQLServerVirtualNetworkRule, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUniqueAttrValue := shared.CompositeLookupKey(serverName, ruleName) + if sdpItem.UniqueAttributeValue() != expectedUniqueAttrValue { + t.Errorf("Expected unique attribute value %s, got %s", expectedUniqueAttrValue, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.SQLServer.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: serverName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_WithSubnetLink", func(t *testing.T) { + subnetID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/test-subnet" + rule := createAzureSqlServerVirtualNetworkRule(serverName, ruleName, subnetID) + + mockClient := mocks.NewMockSqlServerVirtualNetworkRuleClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, ruleName).Return( + armsql.VirtualNetworkRulesClientGetResponse{ + VirtualNetworkRule: *rule, + }, nil) + + wrapper := manual.NewSqlServerVirtualNetworkRule(&testSqlServerVirtualNetworkRuleClient{MockSqlServerVirtualNetworkRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, ruleName) + 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.SQLServer.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: serverName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, + { + ExpectedType: azureshared.NetworkVirtualNetwork.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-vnet", + ExpectedScope: subscriptionID + "." + resourceGroup, + }, + { + ExpectedType: azureshared.NetworkSubnet.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), + ExpectedScope: subscriptionID + "." + resourceGroup, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSqlServerVirtualNetworkRuleClient(ctrl) + wrapper := manual.NewSqlServerVirtualNetworkRule(&testSqlServerVirtualNetworkRuleClient{MockSqlServerVirtualNetworkRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], serverName, true) + if qErr == nil { + t.Error("Expected error when providing only serverName (1 query part), but got nil") + } + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockSqlServerVirtualNetworkRuleClient(ctrl) + wrapper := manual.NewSqlServerVirtualNetworkRule(&testSqlServerVirtualNetworkRuleClient{MockSqlServerVirtualNetworkRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, "") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when virtual network rule name is empty, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + rule1 := createAzureSqlServerVirtualNetworkRule(serverName, "rule1", "") + rule2 := createAzureSqlServerVirtualNetworkRule(serverName, "rule2", "") + + mockClient := mocks.NewMockSqlServerVirtualNetworkRuleClient(ctrl) + pager := &mockSqlServerVirtualNetworkRulePager{ + pages: []armsql.VirtualNetworkRulesClientListByServerResponse{ + { + VirtualNetworkRuleListResult: armsql.VirtualNetworkRuleListResult{ + Value: []*armsql.VirtualNetworkRule{rule1, rule2}, + }, + }, + }, + } + + testClient := &testSqlServerVirtualNetworkRuleClient{ + MockSqlServerVirtualNetworkRuleClient: mockClient, + pager: pager, + } + wrapper := manual.NewSqlServerVirtualNetworkRule(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") + } + + items, qErr := searchable.Search(ctx, wrapper.Scopes()[0], serverName, true) + if qErr != nil { + t.Fatalf("Expected no error from Search, got: %v", qErr) + } + if len(items) != 2 { + t.Errorf("Expected 2 items from Search, got %d", len(items)) + } + }) + + t.Run("SearchStream", func(t *testing.T) { + rule1 := createAzureSqlServerVirtualNetworkRule(serverName, "rule1", "") + + mockClient := mocks.NewMockSqlServerVirtualNetworkRuleClient(ctrl) + pager := &mockSqlServerVirtualNetworkRulePager{ + pages: []armsql.VirtualNetworkRulesClientListByServerResponse{ + { + VirtualNetworkRuleListResult: armsql.VirtualNetworkRuleListResult{ + Value: []*armsql.VirtualNetworkRule{rule1}, + }, + }, + }, + } + + testClient := &testSqlServerVirtualNetworkRuleClient{ + MockSqlServerVirtualNetworkRuleClient: mockClient, + pager: pager, + } + wrapper := manual.NewSqlServerVirtualNetworkRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchStreamable, ok := adapter.(discovery.SearchStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support SearchStream operation") + } + + stream := discovery.NewRecordingQueryResultStream() + searchStreamable.SearchStream(ctx, wrapper.Scopes()[0], serverName, true, stream) + items := stream.GetItems() + errs := stream.GetErrors() + if len(errs) > 0 { + t.Fatalf("Expected no errors from SearchStream, got: %v", errs) + } + if len(items) != 1 { + t.Errorf("Expected 1 item from SearchStream, got %d", len(items)) + } + }) + + t.Run("SearchWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSqlServerVirtualNetworkRuleClient(ctrl) + wrapper := manual.NewSqlServerVirtualNetworkRule(&testSqlServerVirtualNetworkRuleClient{MockSqlServerVirtualNetworkRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("virtual network rule not found") + + mockClient := mocks.NewMockSqlServerVirtualNetworkRuleClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, "nonexistent-rule").Return( + armsql.VirtualNetworkRulesClientGetResponse{}, expectedErr) + + wrapper := manual.NewSqlServerVirtualNetworkRule(&testSqlServerVirtualNetworkRuleClient{MockSqlServerVirtualNetworkRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, "nonexistent-rule") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent virtual network rule, but got nil") + } + }) + + t.Run("ErrorHandling_Search", func(t *testing.T) { + mockClient := mocks.NewMockSqlServerVirtualNetworkRuleClient(ctrl) + errorPager := &errorSqlServerVirtualNetworkRulePager{} + testClient := &testSqlServerVirtualNetworkRuleClient{ + MockSqlServerVirtualNetworkRuleClient: mockClient, + pager: errorPager, + } + + wrapper := manual.NewSqlServerVirtualNetworkRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0], serverName) + if qErr == nil { + t.Error("Expected error from Search when pager returns error, but got nil") + } + }) + + t.Run("InterfaceCompliance", func(t *testing.T) { + mockClient := mocks.NewMockSqlServerVirtualNetworkRuleClient(ctrl) + wrapper := manual.NewSqlServerVirtualNetworkRule(&testSqlServerVirtualNetworkRuleClient{MockSqlServerVirtualNetworkRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + w := wrapper.(sources.Wrapper) + + permissions := w.IAMPermissions() + if len(permissions) == 0 { + t.Error("Expected IAMPermissions to return at least one permission") + } + expectedPermission := "Microsoft.Sql/servers/virtualNetworkRules/read" + found := slices.Contains(permissions, expectedPermission) + if !found { + t.Errorf("Expected IAMPermissions to include %s", expectedPermission) + } + + potentialLinks := w.PotentialLinks() + if !potentialLinks[azureshared.SQLServer] { + t.Error("Expected PotentialLinks to include SQLServer") + } + if !potentialLinks[azureshared.NetworkSubnet] { + t.Error("Expected PotentialLinks to include NetworkSubnet") + } + if !potentialLinks[azureshared.NetworkVirtualNetwork] { + t.Error("Expected PotentialLinks to include NetworkVirtualNetwork") + } + + mappings := w.TerraformMappings() + if len(mappings) == 0 { + t.Error("Expected TerraformMappings to return at least one mapping") + } + foundMapping := false + for _, mapping := range mappings { + if mapping.GetTerraformQueryMap() == "azurerm_mssql_virtual_network_rule.id" { + foundMapping = true + break + } + } + if !foundMapping { + t.Error("Expected TerraformMappings to include 'azurerm_mssql_virtual_network_rule.id' mapping") + } + }) +} + +func createAzureSqlServerVirtualNetworkRule(serverName, ruleName, subnetID string) *armsql.VirtualNetworkRule { + ruleID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Sql/servers/" + serverName + "/virtualNetworkRules/" + ruleName + rule := &armsql.VirtualNetworkRule{ + Name: &ruleName, + ID: &ruleID, + Properties: &armsql.VirtualNetworkRuleProperties{}, + } + if subnetID != "" { + rule.Properties.VirtualNetworkSubnetID = &subnetID + } + return rule +} diff --git a/sources/azure/shared/mocks/mock_sql_server_virtual_network_rule_client.go b/sources/azure/shared/mocks/mock_sql_server_virtual_network_rule_client.go new file mode 100644 index 00000000..3e512121 --- /dev/null +++ b/sources/azure/shared/mocks/mock_sql_server_virtual_network_rule_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: sql-server-virtual-network-rule-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_sql_server_virtual_network_rule_client.go -package=mocks -source=sql-server-virtual-network-rule-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armsql "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockSqlServerVirtualNetworkRuleClient is a mock of SqlServerVirtualNetworkRuleClient interface. +type MockSqlServerVirtualNetworkRuleClient struct { + ctrl *gomock.Controller + recorder *MockSqlServerVirtualNetworkRuleClientMockRecorder + isgomock struct{} +} + +// MockSqlServerVirtualNetworkRuleClientMockRecorder is the mock recorder for MockSqlServerVirtualNetworkRuleClient. +type MockSqlServerVirtualNetworkRuleClientMockRecorder struct { + mock *MockSqlServerVirtualNetworkRuleClient +} + +// NewMockSqlServerVirtualNetworkRuleClient creates a new mock instance. +func NewMockSqlServerVirtualNetworkRuleClient(ctrl *gomock.Controller) *MockSqlServerVirtualNetworkRuleClient { + mock := &MockSqlServerVirtualNetworkRuleClient{ctrl: ctrl} + mock.recorder = &MockSqlServerVirtualNetworkRuleClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSqlServerVirtualNetworkRuleClient) EXPECT() *MockSqlServerVirtualNetworkRuleClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockSqlServerVirtualNetworkRuleClient) Get(ctx context.Context, resourceGroupName, serverName, virtualNetworkRuleName string) (armsql.VirtualNetworkRulesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, serverName, virtualNetworkRuleName) + ret0, _ := ret[0].(armsql.VirtualNetworkRulesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockSqlServerVirtualNetworkRuleClientMockRecorder) Get(ctx, resourceGroupName, serverName, virtualNetworkRuleName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSqlServerVirtualNetworkRuleClient)(nil).Get), ctx, resourceGroupName, serverName, virtualNetworkRuleName) +} + +// ListByServer mocks base method. +func (m *MockSqlServerVirtualNetworkRuleClient) ListByServer(ctx context.Context, resourceGroupName, serverName string) clients.SqlServerVirtualNetworkRulePager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByServer", ctx, resourceGroupName, serverName) + ret0, _ := ret[0].(clients.SqlServerVirtualNetworkRulePager) + return ret0 +} + +// ListByServer indicates an expected call of ListByServer. +func (mr *MockSqlServerVirtualNetworkRuleClientMockRecorder) ListByServer(ctx, resourceGroupName, serverName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByServer", reflect.TypeOf((*MockSqlServerVirtualNetworkRuleClient)(nil).ListByServer), ctx, resourceGroupName, serverName) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index a2a2b4e7..404c89a3 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -25,6 +25,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "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-sql-server-firewall-rule": {"servers", "firewallRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/firewallRules/{ruleName}", + "azure-sql-server-virtual-network-rule": {"servers", "virtualNetworkRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/virtualNetworkRules/{ruleName}", "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}", From c0c27d6146f92b79d1d4d36dfe1ace17c8c4b6e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 09:33:13 +0100 Subject: [PATCH 22/74] fix(deps): update google.golang.org/genproto/googleapis/rpc digest to a57be14 (#4030) 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 | `2f722ef` → `a57be14` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/370) 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: 12b6ad8bb0959eb7e0f78c6c7baee18bff7ca520 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0013ef1f..33c7d0dc 100644 --- a/go.mod +++ b/go.mod @@ -187,7 +187,7 @@ require ( gonum.org/v1/gonum v0.17.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-20260223185530-2f722ef697dc + google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 gopkg.in/ini.v1 v1.67.1 diff --git a/go.sum b/go.sum index 2398131b..0190ea34 100644 --- a/go.sum +++ b/go.sum @@ -1488,8 +1488,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-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-20260223185530-2f722ef697dc h1:51Wupg8spF+5FC6D+iMKbOddFjMckETnNnEiZ+HX37s= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260223185530-2f722ef697dc/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= 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 cfa1efa693de0adbaaa709120510d9bcb2d35128 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:26:48 +0100 Subject: [PATCH 23/74] fix(deps): update github.com/hashicorp/terraform-config-inspect digest to 813a975 (#4029) 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 | `f4be3ba` → `813a975` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/370) 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: 313b6b8db2626514ba8f5587c0c25763568608c5 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 33c7d0dc..77d54301 100644 --- a/go.mod +++ b/go.mod @@ -115,7 +115,7 @@ require ( 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-config-inspect v0.0.0-20260224005459-813a97530220 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 diff --git a/go.sum b/go.sum index 0190ea34..a1084bbf 100644 --- a/go.sum +++ b/go.sum @@ -658,8 +658,8 @@ github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQx 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-config-inspect v0.0.0-20260224005459-813a97530220 h1:v0h6j7IMgA24b8aWG5+d6WStIP9G8e/p0DKK3Bmk7YQ= +github.com/hashicorp/terraform-config-inspect v0.0.0-20260224005459-813a97530220/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= From 29617466f7a5887964b1f6342164c4e2d1eeee92 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 27 Feb 2026 10:48:33 +0100 Subject: [PATCH 24/74] ci: fix markdownlint CI step and resolve all docs lint violations (#4003) ## Summary - Fix the markdownlint `files:` glob in CI that has been broken since Feb 2025 (missing opening quote), meaning markdownlint never ran on any docs. - Resolve all pre-existing markdownlint violations so the step passes cleanly. - Follow-up to #4001 which identified this issue while fixing lychee. ## Changes | Area | Change | | --- | --- | | `docs.overmind.tech/.markdownlintignore` (new) | Exclude `node_modules/` -- eliminates 632 false positives | | `docs.overmind.tech/.markdownlint.json` | Disable MD024 (duplicate headings), MD033 (inline HTML), MD034 (bare URLs -- angle-bracket autolinks break MDX), MD036 (emphasis-as-heading) | | ~8 docs files | Auto-fixed via `markdownlint --fix`: blank lines around lists, indentation, consecutive blank lines, trailing spaces | | 6 docs files | Manual fixes: added `text` language to 9 bare code fences, fixed 1 ordered list prefix | | `.github/workflows/ci.yml` | Fixed `files:` quoting, added explicit `config_file` and `ignore_path` parameters | ## Notes - **MD034 (bare URLs) is disabled** because markdownlint's auto-fix wraps them in `` syntax, which breaks Docusaurus/MDX (it interprets `<` as JSX). The auto-generated source type docs use bare URLs extensively and they render fine in Docusaurus without angle brackets. - The remaining disabled rules (MD024, MD033, MD036) all conflict with Docusaurus conventions or generated docs patterns. --- > [!NOTE] > **Low Risk** > CI/doc-only changes that primarily affect lint enforcement and markdown formatting, with minimal impact on runtime behavior. > > **Overview** > Re-enables docs markdown linting in CI by fixing the broken `markdownlint-cli` `files` glob and wiring in explicit `config_file` and `ignore_path`. > > Adds/updates markdownlint configuration under `docs.overmind.tech` (new `.markdownlintignore` excluding `node_modules/`, and `.markdownlint.json` disabling several rules) and applies doc formatting fixes (e.g., code fences annotated as `text`, list spacing/numbering) to satisfy the now-enforced lints. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0fa9478c858659e55cb1f06f57a96127fcd33f60. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 148c43d74d2aa242169cca45da70a74672ffc3c4 --- .../docs/sources/aws/update-to-pod-identity.md | 4 +++- docs.overmind.tech/docs/sources/gcp/configuration.md | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) 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 index 64c40288..b6cfd8de 100644 --- a/docs.overmind.tech/docs/sources/aws/update-to-pod-identity.md +++ b/docs.overmind.tech/docs/sources/aws/update-to-pod-identity.md @@ -62,6 +62,7 @@ Look for a stack named "Overmind" or "OvermindDevelopment" in the region where y :::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 @@ -133,7 +134,7 @@ Your complete trust policy should look like this: } ``` -4. Click **"Update policy"** +1. Click **"Update policy"** #### Step 4: Update the Version Tag (Optional) @@ -149,6 +150,7 @@ To help track the version of your role configuration: 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 diff --git a/docs.overmind.tech/docs/sources/gcp/configuration.md b/docs.overmind.tech/docs/sources/gcp/configuration.md index 450d3b11..57643214 100644 --- a/docs.overmind.tech/docs/sources/gcp/configuration.md +++ b/docs.overmind.tech/docs/sources/gcp/configuration.md @@ -142,7 +142,7 @@ Permissions can be applied at any level of the GCP resource hierarchy and are in **Direct Access:** -``` +```text Your GCP Organization/Folder/Project └─ Overmind Service Account └─ Granted: Viewer roles (+ custom role for project-level) @@ -151,7 +151,7 @@ Your GCP Organization/Folder/Project **Service Account Impersonation:** -``` +```text Your GCP Organization/Folder/Project ├─ Your Service Account │ └─ Granted: Viewer roles (+ custom role for project-level) @@ -267,9 +267,11 @@ Re-run the setup script or check for organization-level policies restricting ser 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 @@ -434,6 +436,6 @@ Here are all the predefined GCP roles that Overmind requires, plus the custom ro | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `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). +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`). From 15439758c3f52e6738cbd22a1759395735b04f88 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:55:37 +0000 Subject: [PATCH 25/74] Add PostgreSQL Flexible Server Firewall Rule Client and Adapter (#4040) image > [!NOTE] > **Low Risk** > Primarily additive discovery support for a new Azure resource type (PostgreSQL Flexible Server firewall rules) with minimal impact on existing adapters; risk is limited to potential integration issues in the new paging/get logic and adapter registration. > > **Overview** > Adds first-class discovery support for Azure PostgreSQL Flexible Server firewall rules via a new `PostgreSQLFlexibleServerFirewallRuleClient` and a `NewDBforPostgreSQLFlexibleServerFirewallRule` searchable wrapper (supports `Get`, `Search`, and `SearchStream`). > > Registers the new adapter in `manual/adapters.go`, wires up the Azure SDK `FirewallRulesClient`, and extends Azure resource-ID path key extraction (`shared/utils.go`) so the new item type can be resolved from IDs. Includes generated GoMock client + unit tests covering happy paths and error handling. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a78c18bf1261b682e38060d6555e88ba3264c89b. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 197c21d49473799e3453aafef48ded8eb154aa8f --- ...ql-flexible-server-firewall-rule-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + ...ostgresql-flexible-server-firewall-rule.go | 250 +++++++++++++ ...esql-flexible-server-firewall-rule_test.go | 328 ++++++++++++++++++ ...ql_flexible_server_firewall_rule_client.go | 72 ++++ sources/azure/shared/utils.go | 3 +- 6 files changed, 697 insertions(+), 1 deletion(-) create mode 100644 sources/azure/clients/postgresql-flexible-server-firewall-rule-client.go create mode 100644 sources/azure/manual/dbforpostgresql-flexible-server-firewall-rule.go create mode 100644 sources/azure/manual/dbforpostgresql-flexible-server-firewall-rule_test.go create mode 100644 sources/azure/shared/mocks/mock_postgresql_flexible_server_firewall_rule_client.go diff --git a/sources/azure/clients/postgresql-flexible-server-firewall-rule-client.go b/sources/azure/clients/postgresql-flexible-server-firewall-rule-client.go new file mode 100644 index 00000000..599dfdcd --- /dev/null +++ b/sources/azure/clients/postgresql-flexible-server-firewall-rule-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" +) + +//go:generate mockgen -destination=../shared/mocks/mock_postgresql_flexible_server_firewall_rule_client.go -package=mocks -source=postgresql-flexible-server-firewall-rule-client.go + +// PostgreSQLFlexibleServerFirewallRulePager is a type alias for the generic Pager interface with PostgreSQL flexible server firewall rule response type. +type PostgreSQLFlexibleServerFirewallRulePager = Pager[armpostgresqlflexibleservers.FirewallRulesClientListByServerResponse] + +// PostgreSQLFlexibleServerFirewallRuleClient is an interface for interacting with Azure PostgreSQL flexible server firewall rules. +type PostgreSQLFlexibleServerFirewallRuleClient interface { + ListByServer(ctx context.Context, resourceGroupName string, serverName string) PostgreSQLFlexibleServerFirewallRulePager + Get(ctx context.Context, resourceGroupName string, serverName string, firewallRuleName string) (armpostgresqlflexibleservers.FirewallRulesClientGetResponse, error) +} + +type postgresqlFlexibleServerFirewallRuleClient struct { + client *armpostgresqlflexibleservers.FirewallRulesClient +} + +func (a *postgresqlFlexibleServerFirewallRuleClient) ListByServer(ctx context.Context, resourceGroupName string, serverName string) PostgreSQLFlexibleServerFirewallRulePager { + return a.client.NewListByServerPager(resourceGroupName, serverName, nil) +} + +func (a *postgresqlFlexibleServerFirewallRuleClient) Get(ctx context.Context, resourceGroupName string, serverName string, firewallRuleName string) (armpostgresqlflexibleservers.FirewallRulesClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, serverName, firewallRuleName, nil) +} + +// NewPostgreSQLFlexibleServerFirewallRuleClient creates a new PostgreSQLFlexibleServerFirewallRuleClient from the Azure SDK client. +func NewPostgreSQLFlexibleServerFirewallRuleClient(client *armpostgresqlflexibleservers.FirewallRulesClient) PostgreSQLFlexibleServerFirewallRuleClient { + return &postgresqlFlexibleServerFirewallRuleClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 6a5dc1ce..7ba48b1a 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -235,6 +235,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create postgresql flexible servers client: %w", err) } + postgresqlFirewallRulesClient, err := armpostgresqlflexibleservers.NewFirewallRulesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create postgresql firewall rules client: %w", err) + } + secretsClient, err := armkeyvault.NewSecretsClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create secrets client: %w", err) @@ -458,6 +463,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewPostgreSQLFlexibleServersClient(postgresqlFlexibleServersClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServerFirewallRule( + clients.NewPostgreSQLFlexibleServerFirewallRuleClient(postgresqlFirewallRulesClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewKeyVaultSecret( clients.NewSecretsClient(secretsClient), resourceGroupScopes, @@ -576,6 +585,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewNetworkApplicationGateway(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewSqlServer(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServer(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServerFirewallRule(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewKeyVaultSecret(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewManagedIdentityUserAssignedIdentity(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewAuthorizationRoleAssignment(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/dbforpostgresql-flexible-server-firewall-rule.go b/sources/azure/manual/dbforpostgresql-flexible-server-firewall-rule.go new file mode 100644 index 00000000..eb583710 --- /dev/null +++ b/sources/azure/manual/dbforpostgresql-flexible-server-firewall-rule.go @@ -0,0 +1,250 @@ +package manual + +import ( + "context" + "errors" + + "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" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var DBforPostgreSQLFlexibleServerFirewallRuleLookupByName = shared.NewItemTypeLookup("name", azureshared.DBforPostgreSQLFlexibleServerFirewallRule) + +type dbforPostgreSQLFlexibleServerFirewallRuleWrapper struct { + client clients.PostgreSQLFlexibleServerFirewallRuleClient + + *azureshared.MultiResourceGroupBase +} + +func NewDBforPostgreSQLFlexibleServerFirewallRule(client clients.PostgreSQLFlexibleServerFirewallRuleClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &dbforPostgreSQLFlexibleServerFirewallRuleWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_DATABASE, + azureshared.DBforPostgreSQLFlexibleServerFirewallRule, + ), + } +} + +func (s dbforPostgreSQLFlexibleServerFirewallRuleWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: serverName and firewallRuleName", + Scope: scope, + ItemType: s.Type(), + } + } + serverName := queryParts[0] + firewallRuleName := queryParts[1] + if firewallRuleName == "" { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "firewallRuleName cannot be empty", + Scope: scope, + ItemType: s.Type(), + } + } + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + resp, err := s.client.Get(ctx, rgScope.ResourceGroup, serverName, firewallRuleName) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + return s.azureDBforPostgreSQLFlexibleServerFirewallRuleToSDPItem(&resp.FirewallRule, serverName, firewallRuleName, scope) +} + +func (s dbforPostgreSQLFlexibleServerFirewallRuleWrapper) azureDBforPostgreSQLFlexibleServerFirewallRuleToSDPItem(rule *armpostgresqlflexibleservers.FirewallRule, serverName, firewallRuleName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(rule, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(serverName, firewallRuleName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.DBforPostgreSQLFlexibleServerFirewallRule.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: nil, + } + + // Link to parent PostgreSQL Flexible Server + if rule.ID != nil { + params := azureshared.ExtractPathParamsFromResourceID(*rule.ID, []string{"flexibleServers"}) + if len(params) > 0 { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.DBforPostgreSQLFlexibleServer.String(), + Method: sdp.QueryMethod_GET, + Query: params[0], + Scope: scope, + }, + }) + } + } else { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.DBforPostgreSQLFlexibleServer.String(), + Method: sdp.QueryMethod_GET, + Query: serverName, + Scope: scope, + }, + }) + } + + // Link to stdlib IP items for StartIPAddress and EndIPAddress + if rule.Properties != nil { + if rule.Properties.StartIPAddress != nil && *rule.Properties.StartIPAddress != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *rule.Properties.StartIPAddress, + Scope: "global", + }, + }) + } + if rule.Properties.EndIPAddress != nil && *rule.Properties.EndIPAddress != "" && (rule.Properties.StartIPAddress == nil || *rule.Properties.EndIPAddress != *rule.Properties.StartIPAddress) { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *rule.Properties.EndIPAddress, + Scope: "global", + }, + }) + } + } + + return sdpItem, nil +} + +func (s dbforPostgreSQLFlexibleServerFirewallRuleWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + DBforPostgreSQLFlexibleServerLookupByName, + DBforPostgreSQLFlexibleServerFirewallRuleLookupByName, + } +} + +func (s dbforPostgreSQLFlexibleServerFirewallRuleWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: serverName", + Scope: scope, + ItemType: s.Type(), + } + } + serverName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + pager := s.client.ListByServer(ctx, rgScope.ResourceGroup, serverName) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + for _, rule := range page.Value { + if rule.Name == nil { + continue + } + item, sdpErr := s.azureDBforPostgreSQLFlexibleServerFirewallRuleToSDPItem(rule, serverName, *rule.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (s dbforPostgreSQLFlexibleServerFirewallRuleWrapper) 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 _, rule := range page.Value { + if rule.Name == nil { + continue + } + item, sdpErr := s.azureDBforPostgreSQLFlexibleServerFirewallRuleToSDPItem(rule, serverName, *rule.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (s dbforPostgreSQLFlexibleServerFirewallRuleWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + DBforPostgreSQLFlexibleServerLookupByName, + }, + } +} + +func (s dbforPostgreSQLFlexibleServerFirewallRuleWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.DBforPostgreSQLFlexibleServer: true, + stdlib.NetworkIP: true, + } +} + +func (s dbforPostgreSQLFlexibleServerFirewallRuleWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_postgresql_flexible_server_firewall_rule.id", + }, + } +} + +func (s dbforPostgreSQLFlexibleServerFirewallRuleWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.DBforPostgreSQL/flexibleServers/firewallRules/read", + } +} + +func (s dbforPostgreSQLFlexibleServerFirewallRuleWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/dbforpostgresql-flexible-server-firewall-rule_test.go b/sources/azure/manual/dbforpostgresql-flexible-server-firewall-rule_test.go new file mode 100644 index 00000000..0240280f --- /dev/null +++ b/sources/azure/manual/dbforpostgresql-flexible-server-firewall-rule_test.go @@ -0,0 +1,328 @@ +package manual_test + +import ( + "context" + "errors" + "slices" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" + "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" +) + +type mockPostgreSQLFlexibleServerFirewallRulePager struct { + pages []armpostgresqlflexibleservers.FirewallRulesClientListByServerResponse + index int +} + +func (m *mockPostgreSQLFlexibleServerFirewallRulePager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockPostgreSQLFlexibleServerFirewallRulePager) NextPage(ctx context.Context) (armpostgresqlflexibleservers.FirewallRulesClientListByServerResponse, error) { + if m.index >= len(m.pages) { + return armpostgresqlflexibleservers.FirewallRulesClientListByServerResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type errorPostgreSQLFlexibleServerFirewallRulePager struct{} + +func (e *errorPostgreSQLFlexibleServerFirewallRulePager) More() bool { + return true +} + +func (e *errorPostgreSQLFlexibleServerFirewallRulePager) NextPage(ctx context.Context) (armpostgresqlflexibleservers.FirewallRulesClientListByServerResponse, error) { + return armpostgresqlflexibleservers.FirewallRulesClientListByServerResponse{}, errors.New("pager error") +} + +type testPostgreSQLFlexibleServerFirewallRuleClient struct { + *mocks.MockPostgreSQLFlexibleServerFirewallRuleClient + pager clients.PostgreSQLFlexibleServerFirewallRulePager +} + +func (t *testPostgreSQLFlexibleServerFirewallRuleClient) ListByServer(ctx context.Context, resourceGroupName, serverName string) clients.PostgreSQLFlexibleServerFirewallRulePager { + return t.pager +} + +func TestDBforPostgreSQLFlexibleServerFirewallRule(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + serverName := "test-server" + firewallRuleName := "test-rule" + + t.Run("Get", func(t *testing.T) { + rule := createAzurePostgreSQLFlexibleServerFirewallRule(serverName, firewallRuleName) + + mockClient := mocks.NewMockPostgreSQLFlexibleServerFirewallRuleClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, firewallRuleName).Return( + armpostgresqlflexibleservers.FirewallRulesClientGetResponse{ + FirewallRule: *rule, + }, nil) + + wrapper := manual.NewDBforPostgreSQLFlexibleServerFirewallRule(&testPostgreSQLFlexibleServerFirewallRuleClient{MockPostgreSQLFlexibleServerFirewallRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, firewallRuleName) + 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.DBforPostgreSQLFlexibleServerFirewallRule.String() { + t.Errorf("Expected type %s, got %s", azureshared.DBforPostgreSQLFlexibleServerFirewallRule, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUniqueAttrValue := shared.CompositeLookupKey(serverName, firewallRuleName) + if sdpItem.UniqueAttributeValue() != expectedUniqueAttrValue { + t.Errorf("Expected unique attribute value %s, got %s", expectedUniqueAttrValue, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.DBforPostgreSQLFlexibleServer.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: serverName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, + { + ExpectedType: stdlib.NetworkIP.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "0.0.0.0", + ExpectedScope: "global", + }, + { + ExpectedType: stdlib.NetworkIP.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "255.255.255.255", + ExpectedScope: "global", + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockPostgreSQLFlexibleServerFirewallRuleClient(ctrl) + wrapper := manual.NewDBforPostgreSQLFlexibleServerFirewallRule(&testPostgreSQLFlexibleServerFirewallRuleClient{MockPostgreSQLFlexibleServerFirewallRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], serverName, true) + if qErr == nil { + t.Error("Expected error when providing only serverName (1 query part), but got nil") + } + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockPostgreSQLFlexibleServerFirewallRuleClient(ctrl) + wrapper := manual.NewDBforPostgreSQLFlexibleServerFirewallRule(&testPostgreSQLFlexibleServerFirewallRuleClient{MockPostgreSQLFlexibleServerFirewallRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, "") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when firewall rule name is empty, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + rule1 := createAzurePostgreSQLFlexibleServerFirewallRule(serverName, "rule1") + rule2 := createAzurePostgreSQLFlexibleServerFirewallRule(serverName, "rule2") + + mockClient := mocks.NewMockPostgreSQLFlexibleServerFirewallRuleClient(ctrl) + pager := &mockPostgreSQLFlexibleServerFirewallRulePager{ + pages: []armpostgresqlflexibleservers.FirewallRulesClientListByServerResponse{ + { + FirewallRuleList: armpostgresqlflexibleservers.FirewallRuleList{ + Value: []*armpostgresqlflexibleservers.FirewallRule{rule1, rule2}, + }, + }, + }, + } + + testClient := &testPostgreSQLFlexibleServerFirewallRuleClient{ + MockPostgreSQLFlexibleServerFirewallRuleClient: mockClient, + pager: pager, + } + wrapper := manual.NewDBforPostgreSQLFlexibleServerFirewallRule(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") + } + + items, qErr := searchable.Search(ctx, wrapper.Scopes()[0], serverName, true) + if qErr != nil { + t.Fatalf("Expected no error from Search, got: %v", qErr) + } + if len(items) != 2 { + t.Errorf("Expected 2 items from Search, got %d", len(items)) + } + }) + + t.Run("SearchStream", func(t *testing.T) { + rule1 := createAzurePostgreSQLFlexibleServerFirewallRule(serverName, "rule1") + + mockClient := mocks.NewMockPostgreSQLFlexibleServerFirewallRuleClient(ctrl) + pager := &mockPostgreSQLFlexibleServerFirewallRulePager{ + pages: []armpostgresqlflexibleservers.FirewallRulesClientListByServerResponse{ + { + FirewallRuleList: armpostgresqlflexibleservers.FirewallRuleList{ + Value: []*armpostgresqlflexibleservers.FirewallRule{rule1}, + }, + }, + }, + } + + testClient := &testPostgreSQLFlexibleServerFirewallRuleClient{ + MockPostgreSQLFlexibleServerFirewallRuleClient: mockClient, + pager: pager, + } + wrapper := manual.NewDBforPostgreSQLFlexibleServerFirewallRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchStreamable, ok := adapter.(discovery.SearchStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support SearchStream operation") + } + + stream := discovery.NewRecordingQueryResultStream() + searchStreamable.SearchStream(ctx, wrapper.Scopes()[0], serverName, true, stream) + items := stream.GetItems() + errs := stream.GetErrors() + if len(errs) > 0 { + t.Fatalf("Expected no errors from SearchStream, got: %v", errs) + } + if len(items) != 1 { + t.Errorf("Expected 1 item from SearchStream, got %d", len(items)) + } + }) + + t.Run("SearchWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockPostgreSQLFlexibleServerFirewallRuleClient(ctrl) + wrapper := manual.NewDBforPostgreSQLFlexibleServerFirewallRule(&testPostgreSQLFlexibleServerFirewallRuleClient{MockPostgreSQLFlexibleServerFirewallRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("firewall rule not found") + + mockClient := mocks.NewMockPostgreSQLFlexibleServerFirewallRuleClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, "nonexistent-rule").Return( + armpostgresqlflexibleservers.FirewallRulesClientGetResponse{}, expectedErr) + + wrapper := manual.NewDBforPostgreSQLFlexibleServerFirewallRule(&testPostgreSQLFlexibleServerFirewallRuleClient{MockPostgreSQLFlexibleServerFirewallRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, "nonexistent-rule") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent firewall rule, but got nil") + } + }) + + t.Run("ErrorHandling_Search", func(t *testing.T) { + mockClient := mocks.NewMockPostgreSQLFlexibleServerFirewallRuleClient(ctrl) + errorPager := &errorPostgreSQLFlexibleServerFirewallRulePager{} + testClient := &testPostgreSQLFlexibleServerFirewallRuleClient{ + MockPostgreSQLFlexibleServerFirewallRuleClient: mockClient, + pager: errorPager, + } + + wrapper := manual.NewDBforPostgreSQLFlexibleServerFirewallRule(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0], serverName) + if qErr == nil { + t.Error("Expected error from Search when pager returns error, but got nil") + } + }) + + t.Run("InterfaceCompliance", func(t *testing.T) { + mockClient := mocks.NewMockPostgreSQLFlexibleServerFirewallRuleClient(ctrl) + wrapper := manual.NewDBforPostgreSQLFlexibleServerFirewallRule(&testPostgreSQLFlexibleServerFirewallRuleClient{MockPostgreSQLFlexibleServerFirewallRuleClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + w := wrapper.(sources.Wrapper) + + permissions := w.IAMPermissions() + if len(permissions) == 0 { + t.Error("Expected IAMPermissions to return at least one permission") + } + expectedPermission := "Microsoft.DBforPostgreSQL/flexibleServers/firewallRules/read" + found := slices.Contains(permissions, expectedPermission) + if !found { + t.Errorf("Expected IAMPermissions to include %s", expectedPermission) + } + + potentialLinks := w.PotentialLinks() + if !potentialLinks[azureshared.DBforPostgreSQLFlexibleServer] { + t.Error("Expected PotentialLinks to include DBforPostgreSQLFlexibleServer") + } + if !potentialLinks[stdlib.NetworkIP] { + t.Error("Expected PotentialLinks to include stdlib.NetworkIP") + } + + mappings := w.TerraformMappings() + if len(mappings) == 0 { + t.Error("Expected TerraformMappings to return at least one mapping") + } + foundMapping := false + for _, mapping := range mappings { + if mapping.GetTerraformQueryMap() == "azurerm_postgresql_flexible_server_firewall_rule.id" { + foundMapping = true + break + } + } + if !foundMapping { + t.Error("Expected TerraformMappings to include 'azurerm_postgresql_flexible_server_firewall_rule.id' mapping") + } + }) +} + +func createAzurePostgreSQLFlexibleServerFirewallRule(serverName, firewallRuleName string) *armpostgresqlflexibleservers.FirewallRule { + ruleID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/" + serverName + "/firewallRules/" + firewallRuleName + return &armpostgresqlflexibleservers.FirewallRule{ + Name: stringPtr(firewallRuleName), + ID: stringPtr(ruleID), + Properties: &armpostgresqlflexibleservers.FirewallRuleProperties{ + StartIPAddress: stringPtr("0.0.0.0"), + EndIPAddress: stringPtr("255.255.255.255"), + }, + } +} + +func stringPtr(s string) *string { + return &s +} diff --git a/sources/azure/shared/mocks/mock_postgresql_flexible_server_firewall_rule_client.go b/sources/azure/shared/mocks/mock_postgresql_flexible_server_firewall_rule_client.go new file mode 100644 index 00000000..a5030f7a --- /dev/null +++ b/sources/azure/shared/mocks/mock_postgresql_flexible_server_firewall_rule_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: postgresql-flexible-server-firewall-rule-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_postgresql_flexible_server_firewall_rule_client.go -package=mocks -source=postgresql-flexible-server-firewall-rule-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armpostgresqlflexibleservers "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockPostgreSQLFlexibleServerFirewallRuleClient is a mock of PostgreSQLFlexibleServerFirewallRuleClient interface. +type MockPostgreSQLFlexibleServerFirewallRuleClient struct { + ctrl *gomock.Controller + recorder *MockPostgreSQLFlexibleServerFirewallRuleClientMockRecorder + isgomock struct{} +} + +// MockPostgreSQLFlexibleServerFirewallRuleClientMockRecorder is the mock recorder for MockPostgreSQLFlexibleServerFirewallRuleClient. +type MockPostgreSQLFlexibleServerFirewallRuleClientMockRecorder struct { + mock *MockPostgreSQLFlexibleServerFirewallRuleClient +} + +// NewMockPostgreSQLFlexibleServerFirewallRuleClient creates a new mock instance. +func NewMockPostgreSQLFlexibleServerFirewallRuleClient(ctrl *gomock.Controller) *MockPostgreSQLFlexibleServerFirewallRuleClient { + mock := &MockPostgreSQLFlexibleServerFirewallRuleClient{ctrl: ctrl} + mock.recorder = &MockPostgreSQLFlexibleServerFirewallRuleClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPostgreSQLFlexibleServerFirewallRuleClient) EXPECT() *MockPostgreSQLFlexibleServerFirewallRuleClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockPostgreSQLFlexibleServerFirewallRuleClient) Get(ctx context.Context, resourceGroupName, serverName, firewallRuleName string) (armpostgresqlflexibleservers.FirewallRulesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, serverName, firewallRuleName) + ret0, _ := ret[0].(armpostgresqlflexibleservers.FirewallRulesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockPostgreSQLFlexibleServerFirewallRuleClientMockRecorder) Get(ctx, resourceGroupName, serverName, firewallRuleName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPostgreSQLFlexibleServerFirewallRuleClient)(nil).Get), ctx, resourceGroupName, serverName, firewallRuleName) +} + +// ListByServer mocks base method. +func (m *MockPostgreSQLFlexibleServerFirewallRuleClient) ListByServer(ctx context.Context, resourceGroupName, serverName string) clients.PostgreSQLFlexibleServerFirewallRulePager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByServer", ctx, resourceGroupName, serverName) + ret0, _ := ret[0].(clients.PostgreSQLFlexibleServerFirewallRulePager) + return ret0 +} + +// ListByServer indicates an expected call of ListByServer. +func (mr *MockPostgreSQLFlexibleServerFirewallRuleClientMockRecorder) ListByServer(ctx, resourceGroupName, serverName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByServer", reflect.TypeOf((*MockPostgreSQLFlexibleServerFirewallRuleClient)(nil).ListByServer), ctx, resourceGroupName, serverName) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 404c89a3..128ec2ad 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -26,7 +26,8 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-sql-database": {"servers", "databases"}, // "/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/Default-SQL-SouthEastAsia/providers/Microsoft.Sql/servers/testsvr/databases/testdb", "azure-sql-server-firewall-rule": {"servers", "firewallRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/firewallRules/{ruleName}", "azure-sql-server-virtual-network-rule": {"servers", "virtualNetworkRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/virtualNetworkRules/{ruleName}", - "azure-dbforpostgresql-database": {"flexibleServers", "databases"}, // "/subscriptions/00000000-1111-2222-3333-444444444444/resourceGroups/Default-PostgreSQL-SouthEastAsia/providers/Microsoft.DBforPostgreSQL/flexibleServers/testsvr/databases/testdb", + "azure-dbforpostgresql-database": {"flexibleServers", "databases"}, // "/subscriptions/.../Microsoft.DBforPostgreSQL/flexibleServers/{server}/databases/{db}", + "azure-dbforpostgresql-flexible-server-firewall-rule": {"flexibleServers", "firewallRules"}, // "/subscriptions/.../Microsoft.DBforPostgreSQL/flexibleServers/{server}/firewallRules/{rule}", "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}", From cbbe5bf178b368d6e7f9974e3766f16375814e7b Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 27 Feb 2026 13:29:25 +0100 Subject: [PATCH 26/74] Add a CI check for go fix (#4023) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > [!NOTE] > **Low Risk** > Mostly adds a CI guardrail and applies mechanical Go modernizations (e.g., `any`, `reflect.TypeFor`) with no intended behavior change; main risk is CI becoming stricter and failing when `go fix` output isn’t committed. > > **Overview** > Adds a new CI job, `go-fix`, that runs `go fix ./...` and fails the workflow if it produces uncommitted diffs, and wires it into `ci-gate`. > > Applies the resulting mechanical Go updates across a few Azure/GCP files (e.g., `interface{}` → `any`, gomock recorder type registration using `reflect.TypeFor`, and cleanup of ad-hoc string pointer helpers in tests). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 29aee6b9eee814a688e732d35e8ce3415c4dacf4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 0353d6400711465ee4f8bd9ba5761393074f0932 --- .../manual/network-private-endpoint_test.go | 30 +++++++++---------- sources/azure/manual/network-route_test.go | 4 +-- .../manual/network-security-rule_test.go | 2 +- .../network-virtual-network-peering_test.go | 12 +++----- .../manual/storage-encryption-scope_test.go | 12 ++++---- .../gcp/dynamic/adapters/compute-firewall.go | 2 +- sources/gcp/manual/compute-instance.go | 4 +-- 7 files changed, 31 insertions(+), 35 deletions(-) diff --git a/sources/azure/manual/network-private-endpoint_test.go b/sources/azure/manual/network-private-endpoint_test.go index 91f33dc2..73ce8c7d 100644 --- a/sources/azure/manual/network-private-endpoint_test.go +++ b/sources/azure/manual/network-private-endpoint_test.go @@ -171,8 +171,8 @@ func TestNetworkPrivateEndpoint(t *testing.T) { pe1 := createAzurePrivateEndpoint("test-pe-1", subscriptionID, resourceGroup) pe2 := &armnetwork.PrivateEndpoint{ Name: nil, - Location: to.Ptr("eastus"), - Tags: map[string]*string{"env": to.Ptr("test")}, + Location: new("eastus"), + Tags: map[string]*string{"env": new("test")}, Properties: &armnetwork.PrivateEndpointProperties{ ProvisioningState: to.Ptr(armnetwork.ProvisioningStateSucceeded), }, @@ -277,7 +277,7 @@ func (m *MockPrivateEndpointsPager) More() bool { func (mr *MockPrivateEndpointsPagerMockRecorder) More() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeOf((*MockPrivateEndpointsPager)(nil).More)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "More", reflect.TypeFor[func() bool]()) } func (m *MockPrivateEndpointsPager) NextPage(ctx context.Context) (armnetwork.PrivateEndpointsClientListResponse, error) { @@ -288,9 +288,9 @@ func (m *MockPrivateEndpointsPager) NextPage(ctx context.Context) (armnetwork.Pr return ret0, ret1 } -func (mr *MockPrivateEndpointsPagerMockRecorder) NextPage(ctx interface{}) *gomock.Call { +func (mr *MockPrivateEndpointsPagerMockRecorder) NextPage(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeOf((*MockPrivateEndpointsPager)(nil).NextPage), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NextPage", reflect.TypeFor[func(ctx context.Context) (armnetwork.PrivateEndpointsClientListResponse, error)](), ctx) } func createAzurePrivateEndpoint(peName, subscriptionID, resourceGroup string) *armnetwork.PrivateEndpoint { @@ -299,34 +299,34 @@ func createAzurePrivateEndpoint(peName, subscriptionID, resourceGroup string) *a asgID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Network/applicationSecurityGroups/test-asg", subscriptionID, resourceGroup) return &armnetwork.PrivateEndpoint{ - Name: to.Ptr(peName), - Location: to.Ptr("eastus"), + Name: new(peName), + Location: new("eastus"), Tags: map[string]*string{ - "env": to.Ptr("test"), - "project": to.Ptr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armnetwork.PrivateEndpointProperties{ ProvisioningState: to.Ptr(armnetwork.ProvisioningStateSucceeded), Subnet: &armnetwork.Subnet{ - ID: to.Ptr(subnetID), + ID: new(subnetID), }, NetworkInterfaces: []*armnetwork.Interface{ - {ID: to.Ptr(nicID)}, + {ID: new(nicID)}, }, ApplicationSecurityGroups: []*armnetwork.ApplicationSecurityGroup{ - {ID: to.Ptr(asgID)}, + {ID: new(asgID)}, }, IPConfigurations: []*armnetwork.PrivateEndpointIPConfiguration{ { Properties: &armnetwork.PrivateEndpointIPConfigurationProperties{ - PrivateIPAddress: to.Ptr("10.0.0.10"), + PrivateIPAddress: new("10.0.0.10"), }, }, }, CustomDNSConfigs: []*armnetwork.CustomDNSConfigPropertiesFormat{ { - Fqdn: to.Ptr("myendpoint.example.com"), - IPAddresses: []*string{to.Ptr("10.0.0.5")}, + Fqdn: new("myendpoint.example.com"), + IPAddresses: []*string{new("10.0.0.5")}, }, }, }, diff --git a/sources/azure/manual/network-route_test.go b/sources/azure/manual/network-route_test.go index a2e25f34..02775600 100644 --- a/sources/azure/manual/network-route_test.go +++ b/sources/azure/manual/network-route_test.go @@ -220,7 +220,7 @@ func TestNetworkRoute(t *testing.T) { { RouteListResult: armnetwork.RouteListResult{ Value: []*armnetwork.Route{ - {Name: nil, ID: strPtr("/some/id")}, + {Name: nil, ID: new("/some/id")}, validRoute, }, }, @@ -299,7 +299,7 @@ func createAzureRoute(routeName, routeTableName string) *armnetwork.Route { Properties: &armnetwork.RoutePropertiesFormat{ ProvisioningState: &provisioningState, NextHopIPAddress: &nextHopIP, - AddressPrefix: strPtr("10.0.0.0/24"), + AddressPrefix: new("10.0.0.0/24"), NextHopType: &nextHopType, }, } diff --git a/sources/azure/manual/network-security-rule_test.go b/sources/azure/manual/network-security-rule_test.go index 5919b827..3f77f922 100644 --- a/sources/azure/manual/network-security-rule_test.go +++ b/sources/azure/manual/network-security-rule_test.go @@ -213,7 +213,7 @@ func TestNetworkSecurityRule(t *testing.T) { { SecurityRuleListResult: armnetwork.SecurityRuleListResult{ Value: []*armnetwork.SecurityRule{ - {Name: nil, ID: strPtr("/some/id")}, + {Name: nil, ID: new("/some/id")}, validRule, }, }, diff --git a/sources/azure/manual/network-virtual-network-peering_test.go b/sources/azure/manual/network-virtual-network-peering_test.go index 136e9184..9ece09bf 100644 --- a/sources/azure/manual/network-virtual-network-peering_test.go +++ b/sources/azure/manual/network-virtual-network-peering_test.go @@ -162,7 +162,7 @@ func TestNetworkVirtualNetworkPeering(t *testing.T) { testClient := &testVirtualNetworkPeeringsClient{ MockVirtualNetworkPeeringsClient: mockClient, - pager: mockPager, + pager: mockPager, } wrapper := manual.NewNetworkVirtualNetworkPeering(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) @@ -213,7 +213,7 @@ func TestNetworkVirtualNetworkPeering(t *testing.T) { { VirtualNetworkPeeringListResult: armnetwork.VirtualNetworkPeeringListResult{ Value: []*armnetwork.VirtualNetworkPeering{ - {Name: nil, ID: strPtr("/some/id")}, + {Name: nil, ID: new("/some/id")}, validPeering, }, }, @@ -223,7 +223,7 @@ func TestNetworkVirtualNetworkPeering(t *testing.T) { testClient := &testVirtualNetworkPeeringsClient{ MockVirtualNetworkPeeringsClient: mockClient, - pager: mockPager, + pager: mockPager, } wrapper := manual.NewNetworkVirtualNetworkPeering(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) @@ -265,7 +265,7 @@ func TestNetworkVirtualNetworkPeering(t *testing.T) { mockClient := mocks.NewMockVirtualNetworkPeeringsClient(ctrl) testClient := &testVirtualNetworkPeeringsClient{ MockVirtualNetworkPeeringsClient: mockClient, - pager: &errorVirtualNetworkPeeringsPager{}, + pager: &errorVirtualNetworkPeeringsPager{}, } wrapper := manual.NewNetworkVirtualNetworkPeering(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) @@ -292,7 +292,3 @@ func createAzureVirtualNetworkPeering(peeringName, vnetName string) *armnetwork. }, } } - -func strPtr(s string) *string { - return &s -} diff --git a/sources/azure/manual/storage-encryption-scope_test.go b/sources/azure/manual/storage-encryption-scope_test.go index 4af992a1..619fdfd0 100644 --- a/sources/azure/manual/storage-encryption-scope_test.go +++ b/sources/azure/manual/storage-encryption-scope_test.go @@ -155,7 +155,7 @@ func TestStorageEncryptionScope(t *testing.T) { testClient := &testEncryptionScopesClient{ MockEncryptionScopesClient: mockClient, - pager: mockPager, + pager: mockPager, } wrapper := manual.NewStorageEncryptionScope(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) @@ -215,7 +215,7 @@ func TestStorageEncryptionScope(t *testing.T) { testClient := &testEncryptionScopesClient{ MockEncryptionScopesClient: mockClient, - pager: mockPager, + pager: mockPager, } wrapper := manual.NewStorageEncryptionScope(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) @@ -264,7 +264,7 @@ func TestStorageEncryptionScope(t *testing.T) { testClient := &testEncryptionScopesClient{ MockEncryptionScopesClient: mockClient, - pager: errorPager, + pager: errorPager, } wrapper := manual.NewStorageEncryptionScope(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) @@ -284,9 +284,9 @@ func TestStorageEncryptionScope(t *testing.T) { func createAzureEncryptionScope(scopeName string) *armstorage.EncryptionScope { return &armstorage.EncryptionScope{ - ID: to.Ptr("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/encryptionScopes/" + scopeName), - Name: to.Ptr(scopeName), - Type: to.Ptr("Microsoft.Storage/storageAccounts/encryptionScopes"), + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/encryptionScopes/" + scopeName), + Name: new(scopeName), + Type: new("Microsoft.Storage/storageAccounts/encryptionScopes"), EncryptionScopeProperties: &armstorage.EncryptionScopeProperties{ Source: to.Ptr(armstorage.EncryptionScopeSourceMicrosoftStorage), State: to.Ptr(armstorage.EncryptionScopeStateEnabled), diff --git a/sources/gcp/dynamic/adapters/compute-firewall.go b/sources/gcp/dynamic/adapters/compute-firewall.go index d3b2d11d..e3ebac5f 100644 --- a/sources/gcp/dynamic/adapters/compute-firewall.go +++ b/sources/gcp/dynamic/adapters/compute-firewall.go @@ -72,7 +72,7 @@ func itemAttributeContainsTag(item *sdp.Item, attrKey, tag string) bool { if err != nil { return false } - list, ok := val.([]interface{}) + list, ok := val.([]any) if !ok { return false } diff --git a/sources/gcp/manual/compute-instance.go b/sources/gcp/manual/compute-instance.go index 9d91004b..32c08658 100644 --- a/sources/gcp/manual/compute-instance.go +++ b/sources/gcp/manual/compute-instance.go @@ -122,7 +122,7 @@ func (c computeInstanceWrapper) Search(ctx context.Context, scope string, queryP if err != nil { continue } - tagsMap, ok := tagsVal.(map[string]interface{}) + tagsMap, ok := tagsVal.(map[string]any) if !ok { continue } @@ -130,7 +130,7 @@ func (c computeInstanceWrapper) Search(ctx context.Context, scope string, queryP if !ok { continue } - itemsList, ok := itemsVal.([]interface{}) + itemsList, ok := itemsVal.([]any) if !ok { continue } From 2aa46e3115176b7c422008f5d54a64f5e7f30ef5 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:37:32 +0000 Subject: [PATCH 27/74] Add Azure Key Vault Key Client and Adapter (#4042) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image > [!NOTE] > **Medium Risk** > Adds a new Azure discovery adapter and wires it into adapter initialization, increasing Key Vault API usage and linked-query graph output. Changes are read-only and well-tested but could impact discovery performance/permissions expectations. > > **Overview** > Adds first-class discovery for **Azure Key Vault keys** via a new `KeysClient` wrapper (with generated GoMock) and a `KeyVaultKey` adapter supporting `GET` (vault+key) and `SEARCH` (list keys by vault). > > Wires the new adapter into `manual/adapters.go` (including real `armkeyvault.NewKeysClient` initialization and placeholder registration), and updates Key Vault vault items to also link to child key searches (and include `KeyVaultKey` in `PotentialLinks`). Also extends Azure resource-ID path key mapping with `azure-keyvault-key`, and adds/updates unit tests to cover the new key adapter behavior and the vault’s additional child link. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0b29a2ddc717149176305b54854346583b95bcc7. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 2bd99fdce98c081a0e3c43d8653b1e85cdf7804a --- sources/azure/clients/keyvault-key-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + sources/azure/manual/keyvault-key.go | 294 ++++++++++ sources/azure/manual/keyvault-key_test.go | 502 ++++++++++++++++++ sources/azure/manual/keyvault-vault.go | 11 +- sources/azure/manual/keyvault-vault_test.go | 14 +- .../shared/mocks/mock_keyvault_key_client.go | 72 +++ sources/azure/shared/utils.go | 1 + 8 files changed, 935 insertions(+), 4 deletions(-) create mode 100644 sources/azure/clients/keyvault-key-client.go create mode 100644 sources/azure/manual/keyvault-key.go create mode 100644 sources/azure/manual/keyvault-key_test.go create mode 100644 sources/azure/shared/mocks/mock_keyvault_key_client.go diff --git a/sources/azure/clients/keyvault-key-client.go b/sources/azure/clients/keyvault-key-client.go new file mode 100644 index 00000000..7f829917 --- /dev/null +++ b/sources/azure/clients/keyvault-key-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" +) + +//go:generate mockgen -destination=../shared/mocks/mock_keyvault_key_client.go -package=mocks -source=keyvault-key-client.go + +// KeysPager is a type alias for the generic Pager interface with keys response type. +type KeysPager = Pager[armkeyvault.KeysClientListResponse] + +// KeysClient is an interface for interacting with Azure Key Vault keys +type KeysClient interface { + NewListPager(resourceGroupName string, vaultName string, options *armkeyvault.KeysClientListOptions) KeysPager + Get(ctx context.Context, resourceGroupName string, vaultName string, keyName string, options *armkeyvault.KeysClientGetOptions) (armkeyvault.KeysClientGetResponse, error) +} + +type keysClient struct { + client *armkeyvault.KeysClient +} + +func (c *keysClient) NewListPager(resourceGroupName string, vaultName string, options *armkeyvault.KeysClientListOptions) KeysPager { + return c.client.NewListPager(resourceGroupName, vaultName, options) +} + +func (c *keysClient) Get(ctx context.Context, resourceGroupName string, vaultName string, keyName string, options *armkeyvault.KeysClientGetOptions) (armkeyvault.KeysClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, vaultName, keyName, options) +} + +// NewKeysClient creates a new KeysClient from the Azure SDK client +func NewKeysClient(client *armkeyvault.KeysClient) KeysClient { + return &keysClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 7ba48b1a..24d7ad41 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -245,6 +245,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create secrets client: %w", err) } + keysClient, err := armkeyvault.NewKeysClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create keys client: %w", err) + } + userAssignedIdentitiesClient, err := armmsi.NewUserAssignedIdentitiesClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create user assigned identities client: %w", err) @@ -471,6 +476,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewSecretsClient(secretsClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewKeyVaultKey( + clients.NewKeysClient(keysClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewManagedIdentityUserAssignedIdentity( clients.NewUserAssignedIdentitiesClient(userAssignedIdentitiesClient), resourceGroupScopes, @@ -587,6 +596,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServer(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServerFirewallRule(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewKeyVaultSecret(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewKeyVaultKey(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewManagedIdentityUserAssignedIdentity(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewAuthorizationRoleAssignment(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeDiskEncryptionSet(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/keyvault-key.go b/sources/azure/manual/keyvault-key.go new file mode 100644 index 00000000..768fb01a --- /dev/null +++ b/sources/azure/manual/keyvault-key.go @@ -0,0 +1,294 @@ +package manual + +import ( + "context" + "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" + "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" +) + +var KeyVaultKeyLookupByName = shared.NewItemTypeLookup("name", azureshared.KeyVaultKey) + +type keyvaultKeyWrapper struct { + client clients.KeysClient + + *azureshared.MultiResourceGroupBase +} + +func NewKeyVaultKey(client clients.KeysClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &keyvaultKeyWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_SECURITY, + azureshared.KeyVaultKey, + ), + } +} + +func (k keyvaultKeyWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, azureshared.QueryError(errors.New("Get requires 2 query parts: vaultName and keyName"), scope, k.Type()) + } + + vaultName := queryParts[0] + if vaultName == "" { + return nil, azureshared.QueryError(errors.New("vaultName cannot be empty"), scope, k.Type()) + } + + keyName := queryParts[1] + if keyName == "" { + return nil, azureshared.QueryError(errors.New("keyName cannot be empty"), scope, k.Type()) + } + + rgScope, err := k.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, k.Type()) + } + resp, err := k.client.Get(ctx, rgScope.ResourceGroup, vaultName, keyName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, k.Type()) + } + + return k.azureKeyToSDPItem(&resp.Key, vaultName, keyName, scope) +} + +func (k keyvaultKeyWrapper) 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: vaultName"), scope, k.Type()) + } + + vaultName := queryParts[0] + if vaultName == "" { + return nil, azureshared.QueryError(errors.New("vaultName cannot be empty"), scope, k.Type()) + } + + rgScope, err := k.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, k.Type()) + } + pager := k.client.NewListPager(rgScope.ResourceGroup, vaultName, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, k.Type()) + } + for _, key := range page.Value { + if key.Name == nil { + continue + } + var keyVaultName string + if key.ID != nil && *key.ID != "" { + vaultParams := azureshared.ExtractPathParamsFromResourceID(*key.ID, []string{"vaults"}) + if len(vaultParams) > 0 { + keyVaultName = vaultParams[0] + } + } + if keyVaultName == "" { + keyVaultName = vaultName + } + item, sdpErr := k.azureKeyToSDPItem(key, keyVaultName, *key.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (k keyvaultKeyWrapper) 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 _, key := range page.Value { + if key.Name == nil { + continue + } + var keyVaultName string + if key.ID != nil && *key.ID != "" { + vaultParams := azureshared.ExtractPathParamsFromResourceID(*key.ID, []string{"vaults"}) + if len(vaultParams) > 0 { + keyVaultName = vaultParams[0] + } + } + if keyVaultName == "" { + keyVaultName = vaultName + } + item, sdpErr := k.azureKeyToSDPItem(key, keyVaultName, *key.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (k keyvaultKeyWrapper) azureKeyToSDPItem(key *armkeyvault.Key, vaultName, keyName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(key, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, k.Type()) + } + + if key.Name == nil { + return nil, azureshared.QueryError(errors.New("key name is nil"), scope, k.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(vaultName, keyName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, k.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.KeyVaultKey.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(key.Tags), + } + + if key.ID != nil && *key.ID != "" { + vaultParams := azureshared.ExtractPathParamsFromResourceID(*key.ID, []string{"vaults"}) + if len(vaultParams) > 0 { + extractedVaultName := vaultParams[0] + if extractedVaultName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(*key.ID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultVault.String(), + Method: sdp.QueryMethod_GET, + Query: extractedVaultName, + Scope: linkedScope, + }, + }) + } + } + } + + var linkedDNSName string + if key.Properties != nil && key.Properties.KeyURI != nil && *key.Properties.KeyURI != "" { + keyURI := *key.Properties.KeyURI + dnsName := azureshared.ExtractDNSFromURL(keyURI) + if dnsName != "" { + linkedDNSName = dnsName + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: dnsName, + Scope: "global", + }, + }) + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkHTTP.String(), + Method: sdp.QueryMethod_SEARCH, + Query: keyURI, + Scope: "global", + }, + }) + } + + if key.Properties != nil && key.Properties.KeyURIWithVersion != nil && *key.Properties.KeyURIWithVersion != "" { + keyURIWithVersion := *key.Properties.KeyURIWithVersion + dnsName := azureshared.ExtractDNSFromURL(keyURIWithVersion) + if dnsName != "" && dnsName != linkedDNSName { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: dnsName, + Scope: "global", + }, + }) + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkHTTP.String(), + Method: sdp.QueryMethod_SEARCH, + Query: keyURIWithVersion, + Scope: "global", + }, + }) + } + + return sdpItem, nil +} + +func (k keyvaultKeyWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + KeyVaultVaultLookupByName, // First key: vault name (queryParts[0]) + KeyVaultKeyLookupByName, // Second key: key name (queryParts[1]) + } +} + +func (k keyvaultKeyWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + KeyVaultVaultLookupByName, + }, + } +} + +func (k keyvaultKeyWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_key_vault_key.id", + }, + } +} + +func (k keyvaultKeyWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.KeyVaultVault, + stdlib.NetworkDNS, + stdlib.NetworkHTTP, + ) +} + +func (k keyvaultKeyWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.KeyVault/vaults/keys/read", + } +} + +func (k keyvaultKeyWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/keyvault-key_test.go b/sources/azure/manual/keyvault-key_test.go new file mode 100644 index 00000000..e989768e --- /dev/null +++ b/sources/azure/manual/keyvault-key_test.go @@ -0,0 +1,502 @@ +package manual_test + +import ( + "context" + "errors" + "fmt" + "slices" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" + "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 newPtr[T any](v T) *T { return &v } + +type mockKeysPager struct { + pages []armkeyvault.KeysClientListResponse + index int +} + +func (m *mockKeysPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockKeysPager) NextPage(ctx context.Context) (armkeyvault.KeysClientListResponse, error) { + if m.index >= len(m.pages) { + return armkeyvault.KeysClientListResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type errorKeysPager struct{} + +func (e *errorKeysPager) More() bool { return true } + +func (e *errorKeysPager) NextPage(ctx context.Context) (armkeyvault.KeysClientListResponse, error) { + return armkeyvault.KeysClientListResponse{}, errors.New("pager error") +} + +type testKeysClient struct { + *mocks.MockKeysClient + pager clients.KeysPager +} + +func (t *testKeysClient) NewListPager(resourceGroupName, vaultName string, options *armkeyvault.KeysClientListOptions) clients.KeysPager { + t.MockKeysClient.NewListPager(resourceGroupName, vaultName, options) + return t.pager +} + +func TestKeyVaultKey(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + vaultName := "test-keyvault" + keyName := "test-key" + + t.Run("Get", func(t *testing.T) { + key := createAzureKey(keyName, subscriptionID, resourceGroup, vaultName) + + mockClient := mocks.NewMockKeysClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, vaultName, keyName, nil).Return( + armkeyvault.KeysClientGetResponse{ + Key: *key, + }, nil) + + wrapper := manual.NewKeyVaultKey(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := vaultName + shared.QuerySeparator + keyName + 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.KeyVaultKey.String() { + t.Errorf("Expected type %s, got %s", azureshared.KeyVaultKey, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUniqueAttrValue := shared.CompositeLookupKey(vaultName, keyName) + if sdpItem.UniqueAttributeValue() != expectedUniqueAttrValue { + t.Errorf("Expected unique attribute value %s, got %s", expectedUniqueAttrValue, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.KeyVaultVault.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: vaultName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, { + ExpectedType: stdlib.NetworkDNS.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: vaultName + ".vault.azure.net", + ExpectedScope: "global", + }, { + ExpectedType: stdlib.NetworkHTTP.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: fmt.Sprintf("https://%s.vault.azure.net/keys/%s", vaultName, keyName), + ExpectedScope: "global", + }} + + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockKeysClient(ctrl) + + wrapper := manual.NewKeyVaultKey(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], vaultName, true) + if qErr == nil { + t.Error("Expected error when providing insufficient query parts, but got nil") + } + }) + + t.Run("Get_EmptyVaultName", func(t *testing.T) { + mockClient := mocks.NewMockKeysClient(ctrl) + + wrapper := manual.NewKeyVaultKey(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.QuerySeparator + keyName + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when vault name is empty, but got nil") + } + }) + + t.Run("Get_EmptyKeyName", func(t *testing.T) { + mockClient := mocks.NewMockKeysClient(ctrl) + + wrapper := manual.NewKeyVaultKey(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := vaultName + shared.QuerySeparator + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when key name is empty, but got nil") + } + }) + + t.Run("Get_NoName", func(t *testing.T) { + key := &armkeyvault.Key{ + Name: nil, + } + + mockClient := mocks.NewMockKeysClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, vaultName, keyName, nil).Return( + armkeyvault.KeysClientGetResponse{ + Key: *key, + }, nil) + + wrapper := manual.NewKeyVaultKey(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := vaultName + shared.QuerySeparator + keyName + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when key has no name, but got nil") + } + }) + + t.Run("Get_NoLinkedResources", func(t *testing.T) { + key := createAzureKeyMinimal(keyName) + + mockClient := mocks.NewMockKeysClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, vaultName, keyName, nil).Return( + armkeyvault.KeysClientGetResponse{ + Key: *key, + }, nil) + + wrapper := manual.NewKeyVaultKey(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := vaultName + shared.QuerySeparator + keyName + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if len(sdpItem.GetLinkedItemQueries()) != 0 { + t.Errorf("Expected no linked item queries, got %d", len(sdpItem.GetLinkedItemQueries())) + } + }) + + t.Run("Search", func(t *testing.T) { + key1 := createAzureKey("key-1", subscriptionID, resourceGroup, vaultName) + key2 := createAzureKey("key-2", subscriptionID, resourceGroup, vaultName) + + mockPager := &mockKeysPager{ + pages: []armkeyvault.KeysClientListResponse{ + { + KeyListResult: armkeyvault.KeyListResult{ + Value: []*armkeyvault.Key{ + {ID: key1.ID, Name: key1.Name, Type: key1.Type, Properties: key1.Properties, Tags: key1.Tags}, + {ID: key2.ID, Name: key2.Name, Type: key2.Type, Properties: key2.Properties, Tags: key2.Tags}, + }, + }, + }, + }, + } + + mockClient := mocks.NewMockKeysClient(ctrl) + mockClient.EXPECT().NewListPager(resourceGroup, vaultName, nil).Return(mockPager) + + testClient := &testKeysClient{ + MockKeysClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewKeyVaultKey(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, wrapper.Scopes()[0], vaultName, 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.Fatalf("Expected no validation error, got: %v", err) + } + if item.GetType() != azureshared.KeyVaultKey.String() { + t.Errorf("Expected type %s, got %s", azureshared.KeyVaultKey, item.GetType()) + } + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockKeysClient(ctrl) + testClient := &testKeysClient{MockKeysClient: mockClient} + + wrapper := manual.NewKeyVaultKey(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("Search_EmptyVaultName", func(t *testing.T) { + mockClient := mocks.NewMockKeysClient(ctrl) + testClient := &testKeysClient{MockKeysClient: mockClient} + + wrapper := manual.NewKeyVaultKey(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0], "") + if qErr == nil { + t.Error("Expected error when vault name is empty, but got nil") + } + }) + + t.Run("Search_KeyWithNilName", func(t *testing.T) { + validKey := createAzureKey("valid-key", subscriptionID, resourceGroup, vaultName) + mockPager := &mockKeysPager{ + pages: []armkeyvault.KeysClientListResponse{ + { + KeyListResult: armkeyvault.KeyListResult{ + Value: []*armkeyvault.Key{ + {Name: nil}, + {ID: validKey.ID, Name: validKey.Name, Type: validKey.Type, Properties: validKey.Properties, Tags: validKey.Tags}, + }, + }, + }, + }, + } + + mockClient := mocks.NewMockKeysClient(ctrl) + mockClient.EXPECT().NewListPager(resourceGroup, vaultName, nil).Return(mockPager) + + testClient := &testKeysClient{ + MockKeysClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewKeyVaultKey(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, wrapper.Scopes()[0], vaultName, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(sdpItems) != 1 { + t.Fatalf("Expected 1 item, got: %d", len(sdpItems)) + } + + expectedUniqueAttrValue := shared.CompositeLookupKey(vaultName, "valid-key") + if sdpItems[0].UniqueAttributeValue() != expectedUniqueAttrValue { + t.Errorf("Expected unique attribute value %s, got %s", expectedUniqueAttrValue, sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("key not found") + + mockClient := mocks.NewMockKeysClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, vaultName, "nonexistent-key", nil).Return( + armkeyvault.KeysClientGetResponse{}, expectedErr) + + wrapper := manual.NewKeyVaultKey(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := vaultName + shared.QuerySeparator + "nonexistent-key" + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent key, but got nil") + } + }) + + t.Run("ErrorHandling_Search", func(t *testing.T) { + mockClient := mocks.NewMockKeysClient(ctrl) + errorPager := &errorKeysPager{} + + mockClient.EXPECT().NewListPager(resourceGroup, vaultName, nil).Return(errorPager) + + testClient := &testKeysClient{ + MockKeysClient: mockClient, + pager: errorPager, + } + + wrapper := manual.NewKeyVaultKey(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, wrapper.Scopes()[0], vaultName, true) + if err == nil { + t.Error("Expected error from pager when NextPage returns an error, but got nil") + } + }) + + t.Run("InterfaceCompliance", func(t *testing.T) { + mockClient := mocks.NewMockKeysClient(ctrl) + testClient := &testKeysClient{MockKeysClient: mockClient} + wrapper := manual.NewKeyVaultKey(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + if wrapper == nil { + t.Error("Wrapper should not be nil") + } + + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + _, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Error("Adapter should implement SearchableAdapter interface") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := mocks.NewMockKeysClient(ctrl) + testClient := &testKeysClient{MockKeysClient: mockClient} + wrapper := manual.NewKeyVaultKey(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + links := wrapper.PotentialLinks() + if len(links) == 0 { + t.Error("Expected potential links to be defined") + } + if !links[azureshared.KeyVaultVault] { + t.Error("Expected KeyVaultVault to be in potential links") + } + if !links[stdlib.NetworkDNS] { + t.Error("Expected stdlib.NetworkDNS to be in potential links") + } + if !links[stdlib.NetworkHTTP] { + t.Error("Expected stdlib.NetworkHTTP to be in potential links") + } + }) + + t.Run("TerraformMappings", func(t *testing.T) { + mockClient := mocks.NewMockKeysClient(ctrl) + testClient := &testKeysClient{MockKeysClient: mockClient} + wrapper := manual.NewKeyVaultKey(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + mappings := wrapper.TerraformMappings() + if len(mappings) == 0 { + t.Fatal("Expected TerraformMappings to be defined") + } + + foundIDMapping := false + for _, mapping := range mappings { + if mapping.GetTerraformQueryMap() == "azurerm_key_vault_key.id" { + foundIDMapping = true + if mapping.GetTerraformMethod() != sdp.QueryMethod_SEARCH { + t.Errorf("Expected TerraformMethod to be SEARCH for id mapping, got %s", mapping.GetTerraformMethod()) + } + } + } + if !foundIDMapping { + t.Error("Expected TerraformMappings to include 'azurerm_key_vault_key.id' mapping") + } + if len(mappings) != 1 { + t.Errorf("Expected 1 TerraformMapping, got %d", len(mappings)) + } + }) + + t.Run("IAMPermissions", func(t *testing.T) { + mockClient := mocks.NewMockKeysClient(ctrl) + testClient := &testKeysClient{MockKeysClient: mockClient} + wrapper := manual.NewKeyVaultKey(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + permissions := wrapper.IAMPermissions() + if len(permissions) == 0 { + t.Error("Expected IAMPermissions to be defined") + } + expectedPermission := "Microsoft.KeyVault/vaults/keys/read" + if !slices.Contains(permissions, expectedPermission) { + t.Errorf("Expected IAMPermissions to include %s", expectedPermission) + } + }) + + t.Run("PredefinedRole", func(t *testing.T) { + mockClient := mocks.NewMockKeysClient(ctrl) + testClient := &testKeysClient{MockKeysClient: mockClient} + wrapper := manual.NewKeyVaultKey(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + type predefinedRoleInterface interface { + PredefinedRole() string + } + if roleInterface, ok := wrapper.(predefinedRoleInterface); ok { + role := roleInterface.PredefinedRole() + if role != "Reader" { + t.Errorf("Expected PredefinedRole to be 'Reader', got %s", role) + } + } else { + t.Error("Wrapper should implement PredefinedRole method") + } + }) +} + +func createAzureKey(keyName, subscriptionID, resourceGroup, vaultName string) *armkeyvault.Key { + return &armkeyvault.Key{ + ID: newPtr(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.KeyVault/vaults/%s/keys/%s", subscriptionID, resourceGroup, vaultName, keyName)), + Name: newPtr(keyName), + Type: newPtr("Microsoft.KeyVault/vaults/keys"), + Tags: map[string]*string{ + "env": newPtr("test"), + "project": newPtr("testing"), + }, + Properties: &armkeyvault.KeyProperties{ + KeyURI: newPtr(fmt.Sprintf("https://%s.vault.azure.net/keys/%s", vaultName, keyName)), + }, + } +} + +func createAzureKeyMinimal(keyName string) *armkeyvault.Key { + return &armkeyvault.Key{ + Name: newPtr(keyName), + Type: newPtr("Microsoft.KeyVault/vaults/keys"), + Tags: map[string]*string{ + "env": newPtr("test"), + }, + Properties: &armkeyvault.KeyProperties{}, + } +} diff --git a/sources/azure/manual/keyvault-vault.go b/sources/azure/manual/keyvault-vault.go index de66dca1..9aee56a7 100644 --- a/sources/azure/manual/keyvault-vault.go +++ b/sources/azure/manual/keyvault-vault.go @@ -135,7 +135,7 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Tags: azureshared.ConvertAzureTags(vault.Tags), } - // Child resources: list secrets in this vault (Search by vault name) + // Child resources: list secrets and keys in this vault (Search by vault name) vaultName := *vault.Name sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ @@ -145,6 +145,14 @@ func (k keyvaultVaultWrapper) azureKeyVaultToSDPItem(vault *armkeyvault.Vault, s Scope: scope, }, }) + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultKey.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 @@ -302,6 +310,7 @@ func (k keyvaultVaultWrapper) TerraformMappings() []*sdp.TerraformMapping { func (k keyvaultVaultWrapper) PotentialLinks() map[shared.ItemType]bool { return shared.NewItemTypesSet( azureshared.KeyVaultSecret, + azureshared.KeyVaultKey, 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 804adb1e..bb8ac8d0 100644 --- a/sources/azure/manual/keyvault-vault_test.go +++ b/sources/azure/manual/keyvault-vault_test.go @@ -112,6 +112,12 @@ func TestKeyVaultVault(t *testing.T) { ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: vaultName, ExpectedScope: subscriptionID + "." + resourceGroup, + }, { + // Child resources: keys in this vault (SEARCH by vault name) + ExpectedType: azureshared.KeyVaultKey.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: vaultName, + ExpectedScope: subscriptionID + "." + resourceGroup, }, { // Private Endpoint (GET) - same resource group ExpectedType: azureshared.NetworkPrivateEndpoint.String(), @@ -219,9 +225,9 @@ func TestKeyVaultVault(t *testing.T) { t.Fatalf("Expected no error, got: %v", qErr) } - // 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())) + // Should only have the child SEARCH links (secrets and keys in vault); no private endpoints, subnets, etc. + if len(sdpItem.GetLinkedItemQueries()) != 2 { + t.Errorf("Expected 2 linked item queries (KeyVaultSecret and KeyVaultKey SEARCH), got %d", len(sdpItem.GetLinkedItemQueries())) } }) @@ -361,6 +367,8 @@ func TestKeyVaultVault(t *testing.T) { } expectedLinks := map[shared.ItemType]bool{ + azureshared.KeyVaultSecret: true, + azureshared.KeyVaultKey: true, azureshared.NetworkPrivateEndpoint: true, azureshared.NetworkSubnet: true, azureshared.KeyVaultManagedHSM: true, diff --git a/sources/azure/shared/mocks/mock_keyvault_key_client.go b/sources/azure/shared/mocks/mock_keyvault_key_client.go new file mode 100644 index 00000000..1a771be2 --- /dev/null +++ b/sources/azure/shared/mocks/mock_keyvault_key_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: keyvault-key-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_keyvault_key_client.go -package=mocks -source=keyvault-key-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armkeyvault "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockKeysClient is a mock of KeysClient interface. +type MockKeysClient struct { + ctrl *gomock.Controller + recorder *MockKeysClientMockRecorder + isgomock struct{} +} + +// MockKeysClientMockRecorder is the mock recorder for MockKeysClient. +type MockKeysClientMockRecorder struct { + mock *MockKeysClient +} + +// NewMockKeysClient creates a new mock instance. +func NewMockKeysClient(ctrl *gomock.Controller) *MockKeysClient { + mock := &MockKeysClient{ctrl: ctrl} + mock.recorder = &MockKeysClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockKeysClient) EXPECT() *MockKeysClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockKeysClient) Get(ctx context.Context, resourceGroupName, vaultName, keyName string, options *armkeyvault.KeysClientGetOptions) (armkeyvault.KeysClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, vaultName, keyName, options) + ret0, _ := ret[0].(armkeyvault.KeysClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockKeysClientMockRecorder) Get(ctx, resourceGroupName, vaultName, keyName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockKeysClient)(nil).Get), ctx, resourceGroupName, vaultName, keyName, options) +} + +// NewListPager mocks base method. +func (m *MockKeysClient) NewListPager(resourceGroupName, vaultName string, options *armkeyvault.KeysClientListOptions) clients.KeysPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListPager", resourceGroupName, vaultName, options) + ret0, _ := ret[0].(clients.KeysPager) + return ret0 +} + +// NewListPager indicates an expected call of NewListPager. +func (mr *MockKeysClientMockRecorder) NewListPager(resourceGroupName, vaultName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListPager", reflect.TypeOf((*MockKeysClient)(nil).NewListPager), resourceGroupName, vaultName, options) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 128ec2ad..c479dba0 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -29,6 +29,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-dbforpostgresql-database": {"flexibleServers", "databases"}, // "/subscriptions/.../Microsoft.DBforPostgreSQL/flexibleServers/{server}/databases/{db}", "azure-dbforpostgresql-flexible-server-firewall-rule": {"flexibleServers", "firewallRules"}, // "/subscriptions/.../Microsoft.DBforPostgreSQL/flexibleServers/{server}/firewallRules/{rule}", "azure-keyvault-secret": {"vaults", "secrets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{vaultName}/secrets/{secretName}", + "azure-keyvault-key": {"vaults", "keys"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{vaultName}/keys/{keyName}", "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}", From 886f81cd31e8cba5bbde2ef97df5378c69bae90d Mon Sep 17 00:00:00 2001 From: TP Honey Date: Fri, 27 Feb 2026 12:50:47 +0000 Subject: [PATCH 28/74] docs(gcp): fix generated markdown lint issues (#4044) This pull request contains changes generated by a Cursor Cloud Agent

Open in Web Open in Cursor 

--- > [!NOTE] > **Low Risk** > Doc-only changes that mostly adjust Markdown formatting (code fence language, list indentation, trailing newlines) with no runtime impact. Risk is limited to potential minor rendering differences in the docs site. > > **Overview** > Standardizes documentation formatting across the knowledge guide and many GCP `Types` pages. > > Updates include fixing stray/empty list markers, annotating directory-tree code blocks with `text`, normalizing bullet indentation under *Terraform Mappings*, and adding missing trailing newlines to avoid `\ No newline at end of file` issues. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0f79dea5de8251ea40e6feb34517c6da815dd7e8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). Co-authored-by: Cursor Agent GitOrigin-RevId: 0feda22f710d45a1af80217b79d57f20869f2f80 --- .../gcp-ai-platform-batch-prediction-job.md | 3 +-- .../gcp/Types/gcp-ai-platform-custom-job.md | 3 +-- .../gcp/Types/gcp-ai-platform-endpoint.md | 3 +-- ...platform-model-deployment-monitoring-job.md | 3 +-- .../sources/gcp/Types/gcp-ai-platform-model.md | 3 +-- .../gcp/Types/gcp-ai-platform-pipeline-job.md | 3 +-- .../gcp-artifact-registry-docker-image.md | 4 ++-- ...-big-query-data-transfer-transfer-config.md | 4 ++-- .../sources/gcp/Types/gcp-big-query-dataset.md | 10 +++++----- .../sources/gcp/Types/gcp-big-query-routine.md | 4 ++-- .../sources/gcp/Types/gcp-big-query-table.md | 10 +++++----- .../Types/gcp-big-table-admin-app-profile.md | 4 ++-- .../gcp/Types/gcp-big-table-admin-backup.md | 3 +-- .../gcp/Types/gcp-big-table-admin-cluster.md | 3 +-- .../gcp/Types/gcp-big-table-admin-instance.md | 10 +++++----- .../gcp/Types/gcp-big-table-admin-table.md | 10 +++++----- .../gcp-certificate-manager-certificate.md | 4 ++-- .../Types/gcp-cloud-billing-billing-info.md | 2 +- .../sources/gcp/Types/gcp-cloud-build-build.md | 2 +- .../gcp/Types/gcp-cloud-functions-function.md | 4 ++-- .../Types/gcp-cloud-kms-crypto-key-version.md | 4 ++-- .../gcp/Types/gcp-cloud-kms-crypto-key.md | 4 ++-- .../gcp/Types/gcp-cloud-kms-key-ring.md | 4 ++-- .../gcp-cloud-resource-manager-project.md | 3 +-- .../gcp-cloud-resource-manager-tag-value.md | 4 ++-- .../sources/gcp/Types/gcp-compute-address.md | 4 ++-- .../gcp/Types/gcp-compute-autoscaler.md | 4 ++-- .../gcp/Types/gcp-compute-backend-service.md | 6 +++--- .../docs/sources/gcp/Types/gcp-compute-disk.md | 4 ++-- .../Types/gcp-compute-external-vpn-gateway.md | 4 ++-- .../sources/gcp/Types/gcp-compute-firewall.md | 4 ++-- .../gcp/Types/gcp-compute-forwarding-rule.md | 4 ++-- .../gcp/Types/gcp-compute-global-address.md | 4 ++-- .../gcp-compute-global-forwarding-rule.md | 4 ++-- .../gcp/Types/gcp-compute-health-check.md | 6 +++--- .../gcp/Types/gcp-compute-http-health-check.md | 4 ++-- .../sources/gcp/Types/gcp-compute-image.md | 4 ++-- .../gcp-compute-instance-group-manager.md | 4 ++-- .../gcp/Types/gcp-compute-instance-group.md | 4 ++-- .../gcp/Types/gcp-compute-instance-template.md | 4 ++-- .../sources/gcp/Types/gcp-compute-instance.md | 4 ++-- .../gcp/Types/gcp-compute-instant-snapshot.md | 4 ++-- .../gcp/Types/gcp-compute-machine-image.md | 4 ++-- .../gcp-compute-network-endpoint-group.md | 4 ++-- .../sources/gcp/Types/gcp-compute-network.md | 4 ++-- .../gcp/Types/gcp-compute-node-group.md | 6 +++--- .../gcp/Types/gcp-compute-node-template.md | 4 ++-- .../sources/gcp/Types/gcp-compute-project.md | 18 +++++++++--------- .../gcp-compute-public-delegated-prefix.md | 4 ++-- .../gcp/Types/gcp-compute-region-commitment.md | 4 ++-- ...-compute-regional-instance-group-manager.md | 4 ++-- .../gcp/Types/gcp-compute-reservation.md | 4 ++-- .../sources/gcp/Types/gcp-compute-route.md | 4 ++-- .../sources/gcp/Types/gcp-compute-router.md | 4 ++-- .../gcp/Types/gcp-compute-security-policy.md | 4 ++-- .../sources/gcp/Types/gcp-compute-snapshot.md | 4 ++-- .../gcp/Types/gcp-compute-ssl-certificate.md | 4 ++-- .../gcp/Types/gcp-compute-ssl-policy.md | 4 ++-- .../gcp/Types/gcp-compute-subnetwork.md | 4 ++-- .../gcp/Types/gcp-compute-target-http-proxy.md | 4 ++-- .../Types/gcp-compute-target-https-proxy.md | 4 ++-- .../gcp/Types/gcp-compute-target-pool.md | 4 ++-- .../sources/gcp/Types/gcp-compute-url-map.md | 4 ++-- .../gcp/Types/gcp-compute-vpn-gateway.md | 4 ++-- .../gcp/Types/gcp-compute-vpn-tunnel.md | 4 ++-- .../sources/gcp/Types/gcp-container-cluster.md | 4 ++-- .../gcp/Types/gcp-container-node-pool.md | 4 ++-- .../gcp/Types/gcp-dataform-repository.md | 4 ++-- .../gcp/Types/gcp-dataplex-aspect-type.md | 4 ++-- .../gcp/Types/gcp-dataplex-data-scan.md | 4 ++-- .../gcp/Types/gcp-dataplex-entry-group.md | 4 ++-- .../Types/gcp-dataproc-autoscaling-policy.md | 4 ++-- .../sources/gcp/Types/gcp-dataproc-cluster.md | 4 ++-- .../sources/gcp/Types/gcp-dns-managed-zone.md | 4 ++-- .../Types/gcp-essential-contacts-contact.md | 4 ++-- .../sources/gcp/Types/gcp-file-instance.md | 4 ++-- .../docs/sources/gcp/Types/gcp-iam-role.md | 2 +- .../gcp/Types/gcp-iam-service-account-key.md | 4 ++-- .../gcp/Types/gcp-iam-service-account.md | 6 +++--- .../sources/gcp/Types/gcp-logging-bucket.md | 3 +-- .../docs/sources/gcp/Types/gcp-logging-link.md | 3 +-- .../gcp/Types/gcp-logging-saved-query.md | 1 - .../docs/sources/gcp/Types/gcp-logging-sink.md | 2 +- .../gcp/Types/gcp-monitoring-alert-policy.md | 4 ++-- .../Types/gcp-monitoring-custom-dashboard.md | 4 ++-- .../gcp-monitoring-notification-channel.md | 4 ++-- .../sources/gcp/Types/gcp-orgpolicy-policy.md | 4 ++-- .../gcp/Types/gcp-pub-sub-subscription.md | 10 +++++----- .../sources/gcp/Types/gcp-pub-sub-topic.md | 10 +++++----- .../sources/gcp/Types/gcp-redis-instance.md | 4 ++-- .../docs/sources/gcp/Types/gcp-run-revision.md | 3 +-- .../docs/sources/gcp/Types/gcp-run-service.md | 4 ++-- .../gcp/Types/gcp-secret-manager-secret.md | 4 ++-- ...enter-management-security-center-service.md | 3 +-- .../Types/gcp-service-directory-endpoint.md | 4 ++-- .../gcp/Types/gcp-service-usage-service.md | 3 +-- .../sources/gcp/Types/gcp-spanner-database.md | 4 ++-- .../sources/gcp/Types/gcp-spanner-instance.md | 4 ++-- .../gcp/Types/gcp-sql-admin-backup-run.md | 2 +- .../sources/gcp/Types/gcp-sql-admin-backup.md | 2 +- .../gcp/Types/gcp-sql-admin-instance.md | 4 ++-- .../gcp/Types/gcp-storage-bucket-iam-policy.md | 8 ++++---- .../sources/gcp/Types/gcp-storage-bucket.md | 10 +++++----- .../Types/gcp-storage-transfer-transfer-job.md | 4 ++-- 104 files changed, 220 insertions(+), 235 deletions(-) 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 index 6b823b2a..d3d697b4 100644 --- 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 @@ -6,7 +6,6 @@ sidebar_label: gcp-ai-platform-batch-prediction-job A **Batch Prediction Job** in Google Cloud’s AI Platform (now part of Vertex AI) lets you run large-scale, asynchronous inference on a saved Machine Learning model. Instead of serving predictions request-by-request, you supply a dataset stored in Cloud Storage or BigQuery and the service spins up the necessary compute, distributes the workload, writes the predictions to your chosen destination, and then shuts itself down. This is ideal for one-off or periodic scoring of very large datasets. Official documentation: https://cloud.google.com/vertex-ai/docs/predictions/batch-predictions - ## Supported Methods * `GET`: Get a gcp-ai-platform-batch-prediction-job by its "locations|batchPredictionJobs" @@ -41,4 +40,4 @@ The Batch Prediction Job executes under a user-specified or default service acco ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -Cloud Storage buckets are commonly used both for the input artefacts (CSV/JSON/TFRecord files) and for the output prediction files. Any bucket mentioned in the job’s specification is linked to the job. \ No newline at end of file +Cloud Storage buckets are commonly used both for the input artefacts (CSV/JSON/TFRecord files) and for the output prediction files. Any bucket mentioned in the job’s specification is linked to the 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 index cd1c2e43..6f833de2 100644 --- 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 @@ -6,7 +6,6 @@ sidebar_label: gcp-ai-platform-custom-job A Vertex AI / AI Platform Custom Job represents an ad-hoc machine-learning workload that you want Google Cloud to run on managed infrastructure. By pointing the job at a custom container image or a Python package, you can execute training, hyper-parameter tuning or batch-processing logic with fine-grained control over machine types, accelerators, networking and encryption. The job definition is submitted to the `projects.locations.customJobs` API and Google Cloud provisions the required compute, streams logs, stores artefacts and tears the resources down once the job finishes. 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" @@ -37,4 +36,4 @@ Vertex AI executes the workload under a user-specified or default service accoun ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -Training data, intermediate checkpoints and exported models are commonly read from or written to Cloud Storage. The Custom Job specifies bucket URIs (e.g., `gs://my-dataset/*`, `gs://my-model-output/`). Overmind connects the job to each referenced `gcp-storage-bucket` so you can assess data residency and access controls. \ No newline at end of file +Training data, intermediate checkpoints and exported models are commonly read from or written to Cloud Storage. The Custom Job specifies bucket URIs (e.g., `gs://my-dataset/*`, `gs://my-model-output/`). Overmind connects the job to each referenced `gcp-storage-bucket` so you can assess data residency and access controls. 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 index 52998ccf..3f22d39f 100644 --- 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 @@ -6,7 +6,6 @@ sidebar_label: gcp-ai-platform-endpoint A **Google Cloud AI Platform Endpoint** (now part of Vertex AI) is a regional, fully-managed HTTPS entry point that receives online prediction requests and routes them to one or more deployed models. Endpoints let you perform low-latency, autoscaled inference, apply access controls, add request/response logging and attach monitoring jobs. Official documentation: https://cloud.google.com/vertex-ai/docs/predictions/getting-predictions#deploy_model_to_endpoint - ## Supported Methods * `GET`: Get a gcp-ai-platform-endpoint by its "name" @@ -37,4 +36,4 @@ Endpoints can be configured for private service access, allowing prediction traf ### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) -Each deployed model on an endpoint runs under a service account whose permissions govern access to other GCP resources (e.g., storage buckets, KMS keys). The link shows which IAM service account is associated with the endpoint’s runtime. \ No newline at end of file +Each deployed model on an endpoint runs under a service account whose permissions govern access to other GCP resources (e.g., storage buckets, KMS keys). The link shows which IAM service account is associated with the endpoint’s runtime. 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 index 1995c763..b4911e93 100644 --- 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 @@ -6,7 +6,6 @@ sidebar_label: gcp-ai-platform-model-deployment-monitoring-job Google Cloud’s Model Deployment Monitoring Job is a managed Vertex AI (formerly AI Platform) service that continuously analyses a deployed model’s predictions to detect data drift, prediction drift and skew between training and online data. A job is attached to one or more deployed models on an Endpoint and periodically samples incoming predictions, calculates statistics, raises alerts and writes monitoring reports to BigQuery or Cloud Storage. Official documentation: https://cloud.google.com/vertex-ai/docs/model-monitoring/overview - ## Supported Methods * `GET`: Get a gcp-ai-platform-model-deployment-monitoring-job by its "locations|modelDeploymentMonitoringJobs" @@ -37,4 +36,4 @@ Alerting rules created by the job use Cloud Monitoring notification channels (e- ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -When Cloud Storage is selected, the job stores prediction samples, intermediate files and final monitoring reports in a user-provided bucket. \ No newline at end of file +When Cloud Storage is selected, the job stores prediction samples, intermediate files and final monitoring reports in a user-provided bucket. 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 index 994733f6..99e47c18 100644 --- 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 @@ -5,7 +5,6 @@ sidebar_label: gcp-ai-platform-model A GCP AI Platform Model (now part of Vertex AI) is a logical container that holds the metadata and artefacts required to serve machine-learning predictions. A model record points to one or more model versions or container images, the Cloud Storage location of the trained parameters, and optional encryption settings. Models are deployed to Endpoints for online prediction or used directly in batch/streaming inference jobs. 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" @@ -32,4 +31,4 @@ Models can be protected with customer-managed encryption keys (CMEK). Overmind l ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -The model’s artefacts (e.g., SavedModel, scikit-learn pickle, PyTorch state) reside in a Cloud Storage bucket referenced by `artifactUri`. Linking to the bucket reveals data-at-rest location and its IAM policy. \ No newline at end of file +The model’s artefacts (e.g., SavedModel, scikit-learn pickle, PyTorch state) reside in a Cloud Storage bucket referenced by `artifactUri`. Linking to the bucket reveals data-at-rest location and its IAM policy. 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 index cca2e5e6..9d77b26d 100644 --- 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 @@ -6,7 +6,6 @@ sidebar_label: gcp-ai-platform-pipeline-job A GCP AI Platform Pipeline Job (now part of Vertex AI Pipelines) represents a single execution of a machine-learning workflow defined in a Kubeflow/Vertex AI pipeline. The job orchestrates a directed acyclic graph (DAG) of pipeline components such as data preparation, model training and evaluation, and optionally deployment. Each run is stored as a resource that tracks the DAG definition, runtime parameters, execution state, logs and metadata. Official documentation: https://cloud.google.com/vertex-ai/docs/pipelines/introduction - ## Supported Methods * `GET`: Get a gcp-ai-platform-pipeline-job by its "name" @@ -29,4 +28,4 @@ The pipeline job executes under a service account which grants it permissions to ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -Vertex AI Pipelines store pipeline definitions, intermediate artefacts, and output models in Cloud Storage. A pipeline job will reference one or more buckets for source code, artefacts and logging, so Overmind creates links to each bucket it touches. \ No newline at end of file +Vertex AI Pipelines store pipeline definitions, intermediate artefacts, and output models in Cloud Storage. A pipeline job will reference one or more buckets for source code, artefacts and logging, so Overmind creates links to each bucket it touches. 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 index ac75f64a..7bdda6ec 100644 --- 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 @@ -8,10 +8,10 @@ For more information, see the official documentation: https://cloud.google.com/a **Terrafrom Mappings:** - * `google_artifact_registry_docker_image.name` +* `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. \ No newline at end of file +* `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 index 9c6bc4e5..3092a3ed 100644 --- 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 @@ -8,7 +8,7 @@ For a full description of the resource see the Google Cloud documentation: https **Terrafrom Mappings:** - * `google_bigquery_data_transfer_config.id` +* `google_bigquery_data_transfer_config.id` ## Supported Methods @@ -32,4 +32,4 @@ Transfers execute using a dedicated service account (`project-number@gcp-sa-bigq ### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) -A transfer configuration can be set to publish run status notifications to a Pub/Sub topic specified in its `notificationPubsubTopic` field. Overmind links the configuration to that topic so that message-flow and permissions between the two resources can be evaluated. \ No newline at end of file +A transfer configuration can be set to publish run status notifications to a Pub/Sub topic specified in its `notificationPubsubTopic` field. Overmind links the configuration to that topic so that message-flow and permissions between the two resources can be evaluated. 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 index 820d662b..3f6de581 100644 --- 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 @@ -7,10 +7,10 @@ A Google Cloud BigQuery Dataset is a logical container that holds tables, views, **Terrafrom Mappings:** - * `google_bigquery_dataset.dataset_id` - * `google_bigquery_dataset_iam_binding.dataset_id` - * `google_bigquery_dataset_iam_member.dataset_id` - * `google_bigquery_dataset_iam_policy.dataset_id` +* `google_bigquery_dataset.dataset_id` +* `google_bigquery_dataset_iam_binding.dataset_id` +* `google_bigquery_dataset_iam_member.dataset_id` +* `google_bigquery_dataset_iam_policy.dataset_id` ## Supported Methods @@ -38,4 +38,4 @@ If customer-managed encryption is enabled, the dataset (and everything inside it ### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) -Access to a dataset is granted via IAM, often to service accounts. Linked service accounts represent principals that have explicit permissions on the dataset. \ No newline at end of file +Access to a dataset is granted via IAM, often to service accounts. Linked service accounts represent principals that have explicit permissions on the dataset. 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 index 7ccf9fa9..ed6afa3a 100644 --- 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 @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/bigquery/docs/reference/rest/v2 **Terrafrom Mappings:** - * `google_bigquery_routine.id` +* `google_bigquery_routine.id` ## Supported Methods @@ -24,4 +24,4 @@ A routine is always contained within exactly one BigQuery dataset. The link lets ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -If a routine’s SQL references an external table backed by Cloud Storage, or if the routine loads/stages data via the `LOAD DATA` or `EXPORT DATA` statements, the routine implicitly depends on the corresponding Cloud Storage bucket. This link surfaces that dependency so you can assess the impact of bucket-level permissions and lifecycle rules on the routine’s execution. \ No newline at end of file +If a routine’s SQL references an external table backed by Cloud Storage, or if the routine loads/stages data via the `LOAD DATA` or `EXPORT DATA` statements, the routine implicitly depends on the corresponding Cloud Storage bucket. This link surfaces that dependency so you can assess the impact of bucket-level permissions and lifecycle rules on the routine’s execution. 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 index 7620f523..162c3bd5 100644 --- 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 @@ -8,10 +8,10 @@ Official documentation: https://cloud.google.com/bigquery/docs/tables **Terrafrom Mappings:** - * `google_bigquery_table.id` - * `google_bigquery_table_iam_binding.dataset_id` - * `google_bigquery_table_iam_member.dataset_id` - * `google_bigquery_table_iam_policy.dataset_id` +* `google_bigquery_table.id` +* `google_bigquery_table_iam_binding.dataset_id` +* `google_bigquery_table_iam_member.dataset_id` +* `google_bigquery_table_iam_policy.dataset_id` ## Supported Methods @@ -35,4 +35,4 @@ If the table (or its parent dataset) is configured to use customer-managed encry ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -An external BigQuery table may use objects stored in a Cloud Storage bucket as its underlying data source; in that case the table is linked to the bucket holding those objects. \ No newline at end of file +An external BigQuery table may use objects stored in a Cloud Storage bucket as its underlying data source; in that case the table is linked to the bucket holding those objects. 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 index ee8556c7..2de4ace1 100644 --- 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 @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/bigtable/docs/app-profiles **Terrafrom Mappings:** - * `google_bigtable_app_profile.id` +* `google_bigtable_app_profile.id` ## Supported Methods @@ -24,4 +24,4 @@ An App Profile points client traffic towards one or more specific clusters. Each ### [`gcp-big-table-admin-instance`](/sources/gcp/Types/gcp-big-table-admin-instance) -Every App Profile exists inside a single Bigtable instance. Linking to `gcp-big-table-admin-instance` shows the broader configuration—such as replication settings and all clusters—that frames the context in which the App Profile operates. \ No newline at end of file +Every App Profile exists inside a single Bigtable instance. Linking to `gcp-big-table-admin-instance` shows the broader configuration—such as replication settings and all clusters—that frames the context in which the App Profile operates. 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 index ff9a0e3c..8c212548 100644 --- 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 @@ -6,7 +6,6 @@ sidebar_label: gcp-big-table-admin-backup A Cloud Bigtable Admin Backup represents a point-in-time copy of a single Bigtable table that is stored within the same Bigtable cluster for a user-defined retention period. Back-ups allow you to restore data that has been deleted or corrupted without replaying your entire write history, and they can also be copied to other regions for disaster-recovery purposes. The resource is created, managed and deleted through the Cloud Bigtable Admin API. Official documentation: https://cloud.google.com/bigtable/docs/backups - ## Supported Methods * `GET`: Get a gcp-big-table-admin-backup by its "instances|clusters|backups" @@ -29,4 +28,4 @@ A backup is a snapshot of a specific Bigtable table at the moment the backup was ### [`gcp-cloud-kms-crypto-key-version`](/sources/gcp/Types/gcp-cloud-kms-crypto-key-version) -When customer-managed encryption (CMEK) is enabled, the backup’s data is encrypted with a particular Cloud KMS key version. Linking to `gcp-cloud-kms-crypto-key-version` lets you audit encryption lineage and verify that the correct key material is being used for protecting the backup. \ No newline at end of file +When customer-managed encryption (CMEK) is enabled, the backup’s data is encrypted with a particular Cloud KMS key version. Linking to `gcp-cloud-kms-crypto-key-version` lets you audit encryption lineage and verify that the correct key material is being used for protecting 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 index 8c67c466..3bab2c7d 100644 --- 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 @@ -6,7 +6,6 @@ sidebar_label: gcp-big-table-admin-cluster A GCP Bigtable Admin Cluster resource represents the configuration of a single cluster that belongs to a Cloud Bigtable instance. The cluster defines the geographic location where data is stored, the number and type of serving nodes, the storage type (HDD or SSD), autoscaling settings, and any customer-managed encryption keys (CMEK) that protect the data. It is managed through the Cloud Bigtable Admin API, which allows you to create, update, or delete clusters programmatically. For further details, see Google’s official documentation: https://cloud.google.com/bigtable/docs/instances-clusters-nodes - ## Supported Methods * `GET`: Get a gcp-big-table-admin-cluster by its "instances|clusters" @@ -21,4 +20,4 @@ A cluster is always a child of a Bigtable instance. This link represents the par ### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) -If Customer-Managed Encryption Keys (CMEK) are enabled, the cluster’s encryption configuration points to the Cloud KMS CryptoKey that is used to encrypt data at rest. This link captures that dependency between the cluster and the key. \ No newline at end of file +If Customer-Managed Encryption Keys (CMEK) are enabled, the cluster’s encryption configuration points to the Cloud KMS CryptoKey that is used to encrypt data at rest. This link captures that dependency between the cluster and the key. 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 index f6adbe85..64af5caa 100644 --- 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 @@ -7,10 +7,10 @@ Cloud Bigtable instances are the top-level administrative containers for all tab **Terrafrom Mappings:** - * `google_bigtable_instance.name` - * `google_bigtable_instance_iam_binding.instance` - * `google_bigtable_instance_iam_member.instance` - * `google_bigtable_instance_iam_policy.instance` +* `google_bigtable_instance.name` +* `google_bigtable_instance_iam_binding.instance` +* `google_bigtable_instance_iam_member.instance` +* `google_bigtable_instance_iam_policy.instance` ## Supported Methods @@ -22,4 +22,4 @@ Cloud Bigtable instances are the top-level administrative containers for all tab ### [`gcp-big-table-admin-cluster`](/sources/gcp/Types/gcp-big-table-admin-cluster) -Every Bigtable instance is composed of one or more clusters. A `gcp-big-table-admin-cluster` represents the individual cluster resources that reside within, and are owned by, a given Bigtable instance. \ No newline at end of file +Every Bigtable instance is composed of one or more clusters. A `gcp-big-table-admin-cluster` represents the individual cluster resources that reside within, and are owned by, a given Bigtable instance. 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 index f1023ee1..908e5ba7 100644 --- 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 @@ -7,10 +7,10 @@ Google Cloud Bigtable is a scalable NoSQL database service for large analytical **Terrafrom Mappings:** - * `google_bigtable_table.id` - * `google_bigtable_table_iam_binding.instance_name` - * `google_bigtable_table_iam_member.instance_name` - * `google_bigtable_table_iam_policy.instance_name` +* `google_bigtable_table.id` +* `google_bigtable_table_iam_binding.instance_name` +* `google_bigtable_table_iam_member.instance_name` +* `google_bigtable_table_iam_policy.instance_name` ## Supported Methods @@ -30,4 +30,4 @@ Every table is created inside a single Bigtable instance. This link shows the pa ### [`gcp-big-table-admin-table`](/sources/gcp/Types/gcp-big-table-admin-table) -Tables may reference each other indirectly through IAM policies or schema design. Overmind links tables to other tables when such relationships are detected, allowing you to trace dependencies across multiple Bigtable tables within or across instances. \ No newline at end of file +Tables may reference each other indirectly through IAM policies or schema design. Overmind links tables to other tables when such relationships are detected, allowing you to trace dependencies across multiple Bigtable tables within or across instances. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-certificate-manager-certificate.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-certificate-manager-certificate.md index 62956bfb..b957b9e4 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-certificate-manager-certificate.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-certificate-manager-certificate.md @@ -7,10 +7,10 @@ A **GCP Certificate Manager Certificate** represents an SSL/TLS certificate that **Terrafrom Mappings:** - * `google_certificate_manager_certificate.id` +* `google_certificate_manager_certificate.id` ## Supported Methods * `GET`: Get GCP Certificate Manager Certificate by "gcp-certificate-manager-certificate-location|gcp-certificate-manager-certificate-name" * ~~`LIST`~~ -* `SEARCH`: Search for GCP Certificate Manager Certificate by "gcp-certificate-manager-certificate-location" \ No newline at end of file +* `SEARCH`: Search for GCP Certificate Manager Certificate by "gcp-certificate-manager-certificate-location" 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 index a8e0f6ea..62dd35ca 100644 --- 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 @@ -23,4 +23,4 @@ Knowing the contents of this object allows Overmind to determine, for example, w ### [`gcp-cloud-resource-manager-project`](/sources/gcp/Types/gcp-cloud-resource-manager-project) Every Billing Info object belongs to exactly one Cloud Resource Manager Project. -Overmind creates a link from `gcp-cloud-billing-billing-info` → `gcp-cloud-resource-manager-project` so that users can trace the billing configuration back to the workload and other resources that live inside the same project. \ No newline at end of file +Overmind creates a link from `gcp-cloud-billing-billing-info` → `gcp-cloud-resource-manager-project` so that users can trace the billing configuration back to the workload and other resources that live inside the same project. 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 index 8dd0b7a4..435d75a0 100644 --- 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 @@ -36,4 +36,4 @@ Secrets injected into build steps via `secretEnv` or `availableSecrets` are stor ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -Cloud Build can pull its source from a Cloud Storage bucket and write build logs or artefacts back to buckets (e.g. via the `logsBucket` or `artifacts` fields). These buckets appear as related `gcp-storage-bucket` resources. \ No newline at end of file +Cloud Build can pull its source from a Cloud Storage bucket and write build logs or artefacts back to buckets (e.g. via the `logsBucket` or `artifacts` fields). These buckets appear as related `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 index 9a21d973..2100db24 100644 --- 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 @@ -3,7 +3,7 @@ title: GCP Cloud Functions Function sidebar_label: gcp-cloud-functions-function --- -Google Cloud Functions is a server-less execution environment that lets you run event-driven code without provisioning or managing servers. A “Function” is the deployed piece of code together with its configuration (runtime, memory/CPU limits, environment variables, ingress/egress settings, triggers and IAM bindings). Documentation: https://cloud.google.com/functions/docs +Google Cloud Functions is a server-less execution environment that lets you run event-driven code without provisioning or managing servers. A “Function” is the deployed piece of code together with its configuration (runtime, memory/CPU limits, environment variables, ingress/egress settings, triggers and IAM bindings). Documentation: https://cloud.google.com/functions/docs ## Supported Methods @@ -31,4 +31,4 @@ Second-generation Cloud Functions are built and deployed as Cloud Run services u ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -Cloud Storage buckets can be both event sources (object create/delete triggers) and repositories for a function’s source code during deployment. Overmind links the function to any bucket that serves as a trigger or holds its source archive. \ No newline at end of file +Cloud Storage buckets can be both event sources (object create/delete triggers) and repositories for a function’s source code during deployment. Overmind links the function to any bucket that serves as a trigger or holds its source archive. 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 index 1a1470ed..ac86612c 100644 --- 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 @@ -7,7 +7,7 @@ A **Cloud KMS CryptoKeyVersion** is an immutable representation of a single piec **Terrafrom Mappings:** - * `google_kms_crypto_key_version.id` +* `google_kms_crypto_key_version.id` ## Supported Methods @@ -19,4 +19,4 @@ A **Cloud KMS CryptoKeyVersion** is an immutable representation of a single piec ### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) -A CryptoKeyVersion is always a child of a CryptoKey. The `gcp-cloud-kms-crypto-key` resource represents the logical key, while the current item represents a particular version of that key’s material. \ No newline at end of file +A CryptoKeyVersion is always a child of a CryptoKey. The `gcp-cloud-kms-crypto-key` resource represents the logical key, while the current item represents a particular version of that key’s material. 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 index 7d976d33..688b48fd 100644 --- 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 @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/kms/docs/object-hierarchy#key **Terrafrom Mappings:** - * `google_kms_crypto_key.id` +* `google_kms_crypto_key.id` ## Supported Methods @@ -24,4 +24,4 @@ A CryptoKey is the parent of one or more CryptoKeyVersions. Each version contain ### [`gcp-cloud-kms-key-ring`](/sources/gcp/Types/gcp-cloud-kms-key-ring) -Every CryptoKey resides within a Key Ring, which provides a namespace and location boundary. This link shows the Key Ring that owns the CryptoKey, allowing you to trace location-specific compliance requirements or IAM inheritance issues. \ No newline at end of file +Every CryptoKey resides within a Key Ring, which provides a namespace and location boundary. This link shows the Key Ring that owns the CryptoKey, allowing you to trace location-specific compliance requirements or IAM inheritance issues. 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 index e3ce826d..7a7e08c9 100644 --- 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 @@ -8,7 +8,7 @@ For full details, see the official documentation: https://cloud.google.com/kms/d **Terrafrom Mappings:** - * `google_kms_key_ring.id` +* `google_kms_key_ring.id` ## Supported Methods @@ -20,4 +20,4 @@ For full details, see the official documentation: https://cloud.google.com/kms/d ### [`gcp-cloud-kms-crypto-key`](/sources/gcp/Types/gcp-cloud-kms-crypto-key) -Each CryptoKey belongs to exactly one Key Ring. Linking a Key Ring to its child `gcp-cloud-kms-crypto-key` items lets Overmind surface all encryption keys that share the same location and IAM policy, making it easier to assess the blast radius of any permission or configuration changes applied to the Key Ring. \ No newline at end of file +Each CryptoKey belongs to exactly one Key Ring. Linking a Key Ring to its child `gcp-cloud-kms-crypto-key` items lets Overmind surface all encryption keys that share the same location and IAM policy, making it easier to assess the blast radius of any permission or configuration changes applied to the Key Ring. 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 index 011d0d4c..b0f61071 100644 --- 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 @@ -6,9 +6,8 @@ sidebar_label: gcp-cloud-resource-manager-project A Google Cloud Resource Manager Project represents the fundamental organisational unit within Google Cloud Platform (GCP). Every compute, storage or networking asset you create must live inside a Project, which in turn sits under a Folder or Organisation node. Projects provide isolated boundaries for Identity and Access Management (IAM), quotas, billing, API enablement and lifecycle operations such as creation, update, suspension and deletion. By modelling Projects, Overmind can surface risks linked to mis-scoped IAM roles, neglected billing settings or interactions with other resources *before* any change is pushed to production. Official 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`~~ \ No newline at end of file +* ~~`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 index 1ee8ad8a..7f38521a 100644 --- 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 @@ -8,10 +8,10 @@ For a full description of Tag Values and how they fit into the tagging system, r **Terrafrom Mappings:** - * `google_tags_tag_value.name` +* `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. \ No newline at end of file +* `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 index 1d13138f..6dd0eb5d 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-address.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-address.md @@ -7,7 +7,7 @@ A GCP Compute Address is a reserved, static IP address that can be either region **Terrafrom Mappings:** - * `google_compute_address.name` +* `google_compute_address.name` ## Supported Methods @@ -47,4 +47,4 @@ Cloud NAT configurations on a Cloud Router can consume one or more reserved exte ### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) -For regional internal addresses you must specify the subnetwork (IP range) to allocate from. The Compute Address therefore references, and is linked to, the Subnetwork resource. \ No newline at end of file +For regional internal addresses you must specify the subnetwork (IP range) to allocate from. The Compute Address therefore references, and is linked to, the Subnetwork resource. 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 index 15a418de..ac697652 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-autoscaler.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-autoscaler.md @@ -7,7 +7,7 @@ A GCP Compute Autoscaler is a zonal or regional resource that automatically adds **Terrafrom Mappings:** - * `google_compute_autoscaler.name` +* `google_compute_autoscaler.name` ## Supported Methods @@ -19,4 +19,4 @@ A GCP Compute Autoscaler is a zonal or regional resource that automatically adds ### [`gcp-compute-instance-group-manager`](/sources/gcp/Types/gcp-compute-instance-group-manager) -Every autoscaler is attached to exactly one managed instance group; in the GCP API this relationship is expressed through the `target` field, which points to the relevant `instanceGroupManager` resource. Following this link in Overmind reveals which VM instances the autoscaler is responsible for scaling. \ No newline at end of file +Every autoscaler is attached to exactly one managed instance group; in the GCP API this relationship is expressed through the `target` field, which points to the relevant `instanceGroupManager` resource. Following this link in Overmind reveals which VM instances the autoscaler is responsible for scaling. 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 index da87ac33..035e8226 100644 --- 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 @@ -8,8 +8,8 @@ For full details see the official Google Cloud documentation: https://cloud.goog **Terrafrom Mappings:** - * `google_compute_backend_service.name` - * `google_compute_region_backend_service.name` +* `google_compute_backend_service.name` +* `google_compute_region_backend_service.name` ## Supported Methods @@ -41,4 +41,4 @@ Network Endpoint Groups (NEGs) can be configured as backends of a backend servic ### [`gcp-compute-security-policy`](/sources/gcp/Types/gcp-compute-security-policy) -A backend service can optionally attach a Cloud Armor Security Policy to enforce L7 firewall rules, rate limiting, and other protective measures on incoming traffic before it reaches the backends. \ No newline at end of file +A backend service can optionally attach a Cloud Armor Security Policy to enforce L7 firewall rules, rate limiting, and other protective measures on incoming traffic before it reaches the backends. 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 index e5278591..de49b064 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-disk.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-disk.md @@ -7,7 +7,7 @@ A GCP Compute Disk—formally known as a Persistent Disk—is block-level storag **Terrafrom Mappings:** - * `google_compute_disk.name` +* `google_compute_disk.name` ## Supported Methods @@ -43,4 +43,4 @@ Represents traditional snapshots for the disk, enabling point-in-time recovery o ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -If disk snapshots or images are exported to Cloud Storage, this link records the destination bucket holding those exports. \ No newline at end of file +If disk snapshots or images are exported to Cloud Storage, this link records the destination bucket holding those exports. 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 index 23bcdda3..38a9e375 100644 --- 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 @@ -8,10 +8,10 @@ For further details, see the official documentation: https://cloud.google.com/co **Terrafrom Mappings:** - * `google_compute_external_vpn_gateway.name` +* `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`~~ \ No newline at end of file +* ~~`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 index 43f3c50b..747162c1 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-firewall.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-firewall.md @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/vpc/docs/firewalls **Terrafrom Mappings:** - * `google_compute_firewall.name` +* `google_compute_firewall.name` ## Supported Methods @@ -28,4 +28,4 @@ Every firewall rule is created within a specific VPC network. The rule only affe ### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) -Firewall rules can target VM instances by the service account they are running as. When a rule uses the `target_service_accounts` field, it is related to those IAM service accounts. \ No newline at end of file +Firewall rules can target VM instances by the service account they are running as. When a rule uses the `target_service_accounts` field, it is related to those IAM service accounts. 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 index 7a5dd132..e3f6f983 100644 --- 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 @@ -8,7 +8,7 @@ For full details see the official documentation: https://cloud.google.com/load-b **Terrafrom Mappings:** - * `google_compute_forwarding_rule.name` +* `google_compute_forwarding_rule.name` ## Supported Methods @@ -48,4 +48,4 @@ External HTTPS Load Balancer forwarding rules target an HTTPS proxy; this link i ### [`gcp-compute-target-pool`](/sources/gcp/Types/gcp-compute-target-pool) -Legacy Network Load Balancer forwarding rules can point directly to a target pool; the link shows which pool receives the traffic. \ No newline at end of file +Legacy Network Load Balancer forwarding rules can point directly to a target pool; the link shows which pool receives the traffic. 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 index ee1e3364..ce177827 100644 --- 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 @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/compute/docs/ip-addresses/reser **Terrafrom Mappings:** - * `google_compute_global_address.name` +* `google_compute_global_address.name` ## Supported Methods @@ -28,4 +28,4 @@ If the address is carved out of a public delegated prefix that your project cont ### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) -For internal global addresses that are further scoped to a particular subnetwork, Overmind establishes a link to the `gcp-compute-subnetwork` so you can trace which subnet’s routing table and firewall rules apply to traffic destined for the address. \ No newline at end of file +For internal global addresses that are further scoped to a particular subnetwork, Overmind establishes a link to the `gcp-compute-subnetwork` so you can trace which subnet’s routing table and firewall rules apply to traffic destined for the address. 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 index 576441b4..e442cdcb 100644 --- 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 @@ -7,7 +7,7 @@ A Google Cloud Compute Global Forwarding Rule defines a single anycast virtual I **Terrafrom Mappings:** - * `google_compute_global_forwarding_rule.name` +* `google_compute_global_forwarding_rule.name` ## Supported Methods @@ -31,4 +31,4 @@ If the forwarding rule is internal, it is scoped to a particular subnetwork. Und ### [`gcp-compute-target-http-proxy`](/sources/gcp/Types/gcp-compute-target-http-proxy) -For external HTTP(S), SSL or TCP proxy load balancers, the forwarding rule points to a target proxy resource. The proxy terminates the client connection before forwarding to backend services. Linking these resources enables Overmind to trace configuration chains and detect misconfigurations such as SSL policy mismatches or missing backends. \ No newline at end of file +For external HTTP(S), SSL or TCP proxy load balancers, the forwarding rule points to a target proxy resource. The proxy terminates the client connection before forwarding to backend services. Linking these resources enables Overmind to trace configuration chains and detect misconfigurations such as SSL policy mismatches or missing backends. 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 index 0550f69e..6f503707 100644 --- 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 @@ -8,11 +8,11 @@ Official documentation: https://cloud.google.com/load-balancing/docs/health-chec **Terrafrom Mappings:** - * `google_compute_health_check.name` - * `google_compute_region_health_check.name` +* `google_compute_health_check.name` +* `google_compute_region_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`~~ \ No newline at end of file +* ~~`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 index bf563c5c..ff27f4ef 100644 --- 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 @@ -8,10 +8,10 @@ For further details see the official documentation: https://cloud.google.com/com **Terrafrom Mappings:** - * `google_compute_http_health_check.name` +* `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`~~ \ No newline at end of file +* ~~`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 index ae515210..525f34d3 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-image.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-image.md @@ -8,7 +8,7 @@ For full details see the official documentation: https://cloud.google.com/comput **Terrafrom Mappings:** - * `google_compute_image.name` +* `google_compute_image.name` ## Supported Methods @@ -44,4 +44,4 @@ Access to create, deprecate or use an image is controlled through IAM roles. Ove ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -During import or export operations, raw disk files are stored in Cloud Storage. Overmind links an image to the Storage Buckets that hosted its source or export objects, enabling you to trace data residency and clean-up unused artefacts. \ No newline at end of file +During import or export operations, raw disk files are stored in Cloud Storage. Overmind links an image to the Storage Buckets that hosted its source or export objects, enabling you to trace data residency and clean-up unused artefacts. 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 index f4e24bbb..81ad76be 100644 --- 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 @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/compute/docs/instance-groups/cr **Terrafrom Mappings:** - * `google_compute_instance_group_manager.name` +* `google_compute_instance_group_manager.name` ## Supported Methods @@ -36,4 +36,4 @@ The manager uses an Instance Template to define the configuration (machine type, ### [`gcp-compute-target-pool`](/sources/gcp/Types/gcp-compute-target-pool) -For legacy network load balancing, an Instance Group Manager can be configured to automatically add or remove its instances from a Target Pool, enabling them to receive traffic from a forwarding rule. \ No newline at end of file +For legacy network load balancing, an Instance Group Manager can be configured to automatically add or remove its instances from a Target Pool, enabling them to receive traffic from a forwarding rule. 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 index 83c9e203..6f59c0ad 100644 --- 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 @@ -8,7 +8,7 @@ For full details see the official Google Cloud documentation: https://cloud.goog **Terrafrom Mappings:** - * `google_compute_instance_group.name` +* `google_compute_instance_group.name` ## Supported Methods @@ -24,4 +24,4 @@ Every VM in an Instance Group must be attached to a VPC network. Overmind theref ### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) -Within a given VPC network, all VMs in the Instance Group reside in a specific subnetwork. Overmind links the Instance Group to that Subnetwork so you can understand IP address allocation, regional placement and any subnet-specific firewall rules that could impact the instances. \ No newline at end of file +Within a given VPC network, all VMs in the Instance Group reside in a specific subnetwork. Overmind links the Instance Group to that Subnetwork so you can understand IP address allocation, regional placement and any subnet-specific firewall rules that could impact the instances. 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 index 43af8bf9..294e9dc2 100644 --- 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 @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/compute/docs/instance-templates **Terrafrom Mappings:** - * `google_compute_instance_template.name` +* `google_compute_instance_template.name` ## Supported Methods @@ -68,4 +68,4 @@ For each network interface, the template can identify a specific subnetwork, dic ### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) -A service account can be attached in the template so that every VM started from it runs with the same IAM identity and associated OAuth scopes. \ No newline at end of file +A service account can be attached in the template so that every VM started from it runs with the same IAM identity and associated OAuth scopes. 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 index 92dd6a93..f39ff54d 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance.md @@ -7,7 +7,7 @@ A Google Cloud Compute Engine instance is a virtual machine (VM) that runs on Go **Terrafrom Mappings:** - * `google_compute_instance.name` +* `google_compute_instance.name` ## Supported Methods @@ -63,4 +63,4 @@ Each network interface is placed within a subnetwork, assigning the instance its ### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) -An optional service account is attached to the instance, granting it IAM-scoped credentials to access Google APIs. \ No newline at end of file +An optional service account is attached to the instance, granting it IAM-scoped credentials to access Google APIs. 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 index c513cd75..ffd90f5d 100644 --- 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 @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/compute/docs/disks/instant-snap **Terrafrom Mappings:** - * `google_compute_instant_snapshot.name` +* `google_compute_instant_snapshot.name` ## Supported Methods @@ -20,4 +20,4 @@ Official documentation: https://cloud.google.com/compute/docs/disks/instant-snap ### [`gcp-compute-disk`](/sources/gcp/Types/gcp-compute-disk) -An instant snapshot is always sourced from an existing Compute Engine persistent disk. Therefore, each `gcp-compute-instant-snapshot` has a direct parent–child relationship with the `gcp-compute-disk` it captures, and Overmind links the snapshot back to the originating disk to surface dependency and recovery paths. \ No newline at end of file +An instant snapshot is always sourced from an existing Compute Engine persistent disk. Therefore, each `gcp-compute-instant-snapshot` has a direct parent–child relationship with the `gcp-compute-disk` it captures, and Overmind links the snapshot back to the originating disk to surface dependency and recovery paths. 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 index fec5aabe..899ebb1e 100644 --- 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 @@ -7,7 +7,7 @@ A Google Cloud Compute Machine Image is a first-class resource that captures the **Terrafrom Mappings:** - * `google_compute_machine_image.name` +* `google_compute_machine_image.name` ## Supported Methods @@ -47,4 +47,4 @@ The machine image stores the exact subnetwork configuration of each NIC, allowin ### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) -Service accounts attached to the source instance are recorded in the machine image; any VM launched from the image inherits those service account bindings unless overridden. \ No newline at end of file +Service accounts attached to the source instance are recorded in the machine image; any VM launched from the image inherits those service account bindings unless overridden. 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 index 6fe662e9..42dcdecd 100644 --- 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 @@ -7,7 +7,7 @@ A Google Cloud Platform Compute Network Endpoint Group (NEG) is a collection of **Terrafrom Mappings:** - * `google_compute_network_endpoint_group.name` +* `google_compute_network_endpoint_group.name` ## Supported Methods @@ -31,4 +31,4 @@ When a NEG is scoped to a subnetwork (for example for VM or GKE pod endpoints), ### [`gcp-run-service`](/sources/gcp/Types/gcp-run-service) -Serverless NEGs can point to Cloud Run services. This link shows which `gcp-run-service` is exposed through the NEG and subsequently through any HTTP(S) load balancer. \ No newline at end of file +Serverless NEGs can point to Cloud Run services. This link shows which `gcp-run-service` is exposed through the NEG and subsequently through any HTTP(S) load balancer. 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 index a0d2fb75..0ef3dcf8 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network.md @@ -7,7 +7,7 @@ A Google Cloud Platform (GCP) Compute Network—commonly called a Virtual Privat **Terrafrom Mappings:** - * `google_compute_network.name` +* `google_compute_network.name` ## Supported Methods @@ -23,4 +23,4 @@ A Compute Network can be peered with, or shared to, another Compute Network. Ove ### [`gcp-compute-subnetwork`](/sources/gcp/Types/gcp-compute-subnetwork) -Every subnetwork is created inside exactly one VPC network. Overmind therefore links each `gcp-compute-subnetwork` back to its parent `gcp-compute-network`, and conversely shows the network’s collection of subnetworks. \ No newline at end of file +Every subnetwork is created inside exactly one VPC network. Overmind therefore links each `gcp-compute-subnetwork` back to its parent `gcp-compute-network`, and conversely shows the network’s collection of subnetworks. 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 index 1b32823d..38ab8552 100644 --- 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 @@ -7,11 +7,11 @@ A GCP Compute Node Group is a managed collection of sole-tenant nodes that are a **Terrafrom Mappings:** - * `google_compute_node_group.name` - * `google_compute_node_template.name` +* `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" \ No newline at end of file +* `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-node-template.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-template.md index 7ebe1ace..66b49159 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-template.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-template.md @@ -8,7 +8,7 @@ For a full specification of the resource, see the official Google Cloud document **Terrafrom Mappings:** - * `google_compute_node_template.name` +* `google_compute_node_template.name` ## Supported Methods @@ -20,4 +20,4 @@ For a full specification of the resource, see the official Google Cloud document ### [`gcp-compute-node-group`](/sources/gcp/Types/gcp-compute-node-group) -A GCP Compute Node Group consumes a single Node Template. Overmind creates a link from a node group back to the template it references so that you can assess how changes to the template (for example, switching CPU platforms) will affect every node that belongs to the group. \ No newline at end of file +A GCP Compute Node Group consumes a single Node Template. Overmind creates a link from a node group back to the template it references so that you can assess how changes to the template (for example, switching CPU platforms) will affect every node that belongs to the group. 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 index 1f1693c5..374f8b46 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-project.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-project.md @@ -7,14 +7,14 @@ A Google Cloud Project is the fundamental organisational unit in Google Cloud Pl **Terrafrom Mappings:** - * `google_project.project_id` - * `google_compute_shared_vpc_host_project.project` - * `google_compute_shared_vpc_service_project.service_project` - * `google_compute_shared_vpc_service_project.host_project` - * `google_project_iam_binding.project` - * `google_project_iam_member.project` - * `google_project_iam_policy.project` - * `google_project_iam_audit_config.project` +* `google_project.project_id` +* `google_compute_shared_vpc_host_project.project` +* `google_compute_shared_vpc_service_project.service_project` +* `google_compute_shared_vpc_service_project.host_project` +* `google_project_iam_binding.project` +* `google_project_iam_member.project` +* `google_project_iam_policy.project` +* `google_project_iam_audit_config.project` ## Supported Methods @@ -30,4 +30,4 @@ Service accounts are identities that live inside a project. Overmind links a gcp ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -Every Cloud Storage bucket is created within a specific project. Overmind establishes a link from a gcp-storage-bucket back to its gcp-compute-project so you can trace ownership, billing and IAM inheritance for the bucket. \ No newline at end of file +Every Cloud Storage bucket is created within a specific project. Overmind establishes a link from a gcp-storage-bucket back to its gcp-compute-project so you can trace ownership, billing and IAM inheritance for the bucket. 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 index 05371dda..25990faf 100644 --- 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 @@ -8,7 +8,7 @@ For full details, see the official documentation: https://cloud.google.com/vpc/d **Terrafrom Mappings:** - * `google_compute_public_delegated_prefix.id` +* `google_compute_public_delegated_prefix.id` ## Supported Methods @@ -24,4 +24,4 @@ This prefix belongs to and is created within a specific Google Cloud project; th ### [`gcp-compute-public-delegated-prefix`](/sources/gcp/Types/gcp-compute-public-delegated-prefix) -A parent Public Delegated Prefix can be linked to child delegated sub-prefixes (or vice-versa) to represent hierarchy and inheritance of the IP space. \ No newline at end of file +A parent Public Delegated Prefix can be linked to child delegated sub-prefixes (or vice-versa) to represent hierarchy and inheritance of the IP space. 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 index c5b90832..cc43218c 100644 --- 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 @@ -7,7 +7,7 @@ A Compute Region Commitment in Google Cloud Platform (GCP) represents a contract **Terrafrom Mappings:** - * `google_compute_region_commitment.name` +* `google_compute_region_commitment.name` ## Supported Methods @@ -19,4 +19,4 @@ A Compute Region Commitment in Google Cloud Platform (GCP) represents a contract ### [`gcp-compute-reservation`](/sources/gcp/Types/gcp-compute-reservation) -Reservations and commitments often work together: a reservation guarantees that capacity is available, while a commitment provides a discount for that capacity. When Overmind discovers a region commitment it links it to any compute reservations in the same project and region so you can see both the cost commitment and the capacity guarantee in one place. \ No newline at end of file +Reservations and commitments often work together: a reservation guarantees that capacity is available, while a commitment provides a discount for that capacity. When Overmind discovers a region commitment it links it to any compute reservations in the same project and region so you can see both the cost commitment and the capacity guarantee in one place. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-regional-instance-group-manager.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-regional-instance-group-manager.md index 286bf0cb..e66363c7 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-regional-instance-group-manager.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-regional-instance-group-manager.md @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/compute/docs/instance-groups/cr **Terrafrom Mappings:** - * `google_compute_region_instance_group_manager.name` +* `google_compute_region_instance_group_manager.name` ## Supported Methods @@ -36,4 +36,4 @@ Every RIGM points to an Instance Template that defines the machine type, boot di ### [`gcp-compute-target-pool`](/sources/gcp/Types/gcp-compute-target-pool) -For legacy network load balancing, a RIGM can register its instances with a Target Pool so that traffic from a network load balancer is distributed across the managed instances. \ No newline at end of file +For legacy network load balancing, a RIGM can register its instances with a Target Pool so that traffic from a network load balancer is distributed across the managed instances. 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 index ec37581b..7255f463 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-reservation.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-reservation.md @@ -7,7 +7,7 @@ A GCP Compute Reservation is a zonal capacity-planning resource that lets you pr **Terrafrom Mappings:** - * `google_compute_reservation.name` +* `google_compute_reservation.name` ## Supported Methods @@ -19,4 +19,4 @@ A GCP Compute Reservation is a zonal capacity-planning resource that lets you pr ### [`gcp-compute-region-commitment`](/sources/gcp/Types/gcp-compute-region-commitment) -Reservations guarantee capacity, while regional commitments provide sustained-use discounts for that capacity. A reservation created in a zone may be covered by, or contribute to the utilisation of, a regional commitment in the same region, so analysing the commitment alongside the reservation reveals both availability and cost-optimisation aspects of the deployment. \ No newline at end of file +Reservations guarantee capacity, while regional commitments provide sustained-use discounts for that capacity. A reservation created in a zone may be covered by, or contribute to the utilisation of, a regional commitment in the same region, so analysing the commitment alongside the reservation reveals both availability and cost-optimisation aspects of the deployment. 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 index 66164775..f26ad359 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-route.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-route.md @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/vpc/docs/routes **Terrafrom Mappings:** - * `google_compute_route.name` +* `google_compute_route.name` ## Supported Methods @@ -32,4 +32,4 @@ Every route is created inside exactly one VPC network, referenced by the `networ ### [`gcp-compute-vpn-tunnel`](/sources/gcp/Types/gcp-compute-vpn-tunnel) -If `nextHopVpnTunnel` is set, the route forwards matching traffic into a Cloud VPN tunnel. The route is consequently linked to the VPN tunnel resource it targets. \ No newline at end of file +If `nextHopVpnTunnel` is set, the route forwards matching traffic into a Cloud VPN tunnel. The route is consequently linked to the VPN tunnel resource it targets. 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 index 5dd98194..751d8844 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-router.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-router.md @@ -7,7 +7,7 @@ A Google Cloud Compute Router is a fully distributed and managed Border Gateway **Terrafrom Mappings:** - * `google_compute_router.id` +* `google_compute_router.id` ## Supported Methods @@ -27,4 +27,4 @@ Subnets within the parent VPC network can have their routes propagated or learne ### [`gcp-compute-vpn-tunnel`](/sources/gcp/Types/gcp-compute-vpn-tunnel) -When Cloud VPN is configured in dynamic mode, the VPN tunnel relies on a Compute Router to exchange BGP routes with the peer gateway, making the tunnel dependent on, and logically linked to, the corresponding gcp-compute-vpn-tunnel resource. \ No newline at end of file +When Cloud VPN is configured in dynamic mode, the VPN tunnel relies on a Compute Router to exchange BGP routes with the peer gateway, making the tunnel dependent on, and logically linked to, the corresponding gcp-compute-vpn-tunnel resource. 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 index 12e8b263..1cfcb57c 100644 --- 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 @@ -8,10 +8,10 @@ For full details, see the official Google documentation: https://cloud.google.co **Terrafrom Mappings:** - * `google_compute_security_policy.name` +* `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`~~ \ No newline at end of file +* ~~`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 index d06d43a5..4acb029a 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-snapshot.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-snapshot.md @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/compute/docs/disks/create-snaps **Terrafrom Mappings:** - * `google_compute_snapshot.name` +* `google_compute_snapshot.name` ## Supported Methods @@ -28,4 +28,4 @@ Every snapshot originates from a source disk. This link shows which Compute Engi ### [`gcp-compute-instant-snapshot`](/sources/gcp/Types/gcp-compute-instant-snapshot) -An instant snapshot is a fast, crash-consistent capture that can later be converted into a regular snapshot. When such a conversion occurs, Overmind links the resulting standard snapshot to its originating instant snapshot, giving visibility into the lineage of your backups. \ No newline at end of file +An instant snapshot is a fast, crash-consistent capture that can later be converted into a regular snapshot. When such a conversion occurs, Overmind links the resulting standard snapshot to its originating instant snapshot, giving visibility into the lineage of your backups. 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 index ce0c95fc..bbe0edb6 100644 --- 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 @@ -7,10 +7,10 @@ A **Google Compute SSL Certificate** represents an SSL certificate resource that **Terrafrom Mappings:** - * `google_compute_ssl_certificate.name` +* `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`~~ \ No newline at end of file +* ~~`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 index 37ef410f..de1bc455 100644 --- 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 @@ -8,10 +8,10 @@ For detailed information, refer to the official Google Cloud documentation: http **Terrafrom Mappings:** - * `google_compute_ssl_policy.name` +* `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`~~ \ No newline at end of file +* ~~`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 index 3ab7fb97..8065aec1 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-subnetwork.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-subnetwork.md @@ -7,7 +7,7 @@ A GCP Compute Subnetwork is a regional, layer-3 virtual network segment that bel **Terrafrom Mappings:** - * `google_compute_subnetwork.name` +* `google_compute_subnetwork.name` ## Supported Methods @@ -23,4 +23,4 @@ Every subnetwork is created inside exactly one VPC network. This link represents ### [`gcp-compute-public-delegated-prefix`](/sources/gcp/Types/gcp-compute-public-delegated-prefix) -A public delegated prefix can be assigned to a subnetwork so that resources inside the subnet can use public IPv4 addresses from that prefix. This link highlights which delegated prefixes are associated with, or routed through, the subnetwork, helping users trace external IP allocations and their exposure. \ No newline at end of file +A public delegated prefix can be assigned to a subnetwork so that resources inside the subnet can use public IPv4 addresses from that prefix. This link highlights which delegated prefixes are associated with, or routed through, the subnetwork, helping users trace external IP allocations and their exposure. 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 index fcce0a73..0b7c57e6 100644 --- 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 @@ -8,7 +8,7 @@ See the official documentation for full details: https://cloud.google.com/load-b **Terrafrom Mappings:** - * `google_compute_target_http_proxy.name` +* `google_compute_target_http_proxy.name` ## Supported Methods @@ -20,4 +20,4 @@ See the official documentation for full details: https://cloud.google.com/load-b ### [`gcp-compute-url-map`](/sources/gcp/Types/gcp-compute-url-map) -A Target HTTP Proxy must reference exactly one URL map. Overmind uses this link to trace from the proxy to the URL map that defines its routing rules, enabling you to understand and surface any risks associated with misconfigured path matchers or backend services. \ No newline at end of file +A Target HTTP Proxy must reference exactly one URL map. Overmind uses this link to trace from the proxy to the URL map that defines its routing rules, enabling you to understand and surface any risks associated with misconfigured path matchers or backend services. 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 index ea74733a..5ad1026b 100644 --- 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 @@ -8,7 +8,7 @@ For full details see the official documentation: https://cloud.google.com/comput **Terrafrom Mappings:** - * `google_compute_target_https_proxy.name` +* `google_compute_target_https_proxy.name` ## Supported Methods @@ -28,4 +28,4 @@ An optional SSL policy can be attached to a Target HTTPS Proxy to enforce minimu ### [`gcp-compute-url-map`](/sources/gcp/Types/gcp-compute-url-map) -Every Target HTTPS Proxy must point to exactly one URL map, which defines how incoming requests are routed to backend services. Overmind links the URL map so you can trace the full request path and evaluate routing risks before deployment. \ No newline at end of file +Every Target HTTPS Proxy must point to exactly one URL map, which defines how incoming requests are routed to backend services. Overmind links the URL map so you can trace the full request path and evaluate routing risks before deployment. 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 index 6cb8537d..babd509f 100644 --- 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 @@ -7,7 +7,7 @@ A Google Cloud Compute Target Pool is a regional grouping of VM instances that a **Terrafrom Mappings:** - * `google_compute_target_pool.id` +* `google_compute_target_pool.id` ## Supported Methods @@ -27,4 +27,4 @@ Each target pool contains a list of VM instances (`instances` field) that will r ### [`gcp-compute-target-pool`](/sources/gcp/Types/gcp-compute-target-pool) -A target pool can specify another target pool as its `backupPool` to provide fail-over capacity, and it can itself be referenced as a backup by other pools. Overmind surfaces these peer-to-peer relationships between target pools. \ No newline at end of file +A target pool can specify another target pool as its `backupPool` to provide fail-over capacity, and it can itself be referenced as a backup by other pools. Overmind surfaces these peer-to-peer relationships between target pools. 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 index d893e7e5..bd8b873f 100644 --- 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 @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/load-balancing/docs/url-map-con **Terrafrom Mappings:** - * `google_compute_url_map.name` +* `google_compute_url_map.name` ## Supported Methods @@ -20,4 +20,4 @@ Official documentation: https://cloud.google.com/load-balancing/docs/url-map-con ### [`gcp-compute-backend-service`](/sources/gcp/Types/gcp-compute-backend-service) -A URL Map points to one or more backend services as its routing targets. Each rule in the map specifies which `gcp-compute-backend-service` should receive the traffic that matches the rule’s host and path conditions. \ No newline at end of file +A URL Map points to one or more backend services as its routing targets. Each rule in the map specifies which `gcp-compute-backend-service` should receive the traffic that matches the rule’s host and path conditions. 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 index 7ed61fa9..26c75780 100644 --- 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 @@ -8,7 +8,7 @@ See the official Google Cloud documentation for full details: https://cloud.goog **Terrafrom Mappings:** - * `google_compute_ha_vpn_gateway.name` +* `google_compute_ha_vpn_gateway.name` ## Supported Methods @@ -20,4 +20,4 @@ See the official Google Cloud documentation for full details: https://cloud.goog ### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) -Each HA VPN Gateway is created inside a single VPC network. Linking the gateway to its `gcp-compute-network` allows Overmind to trace which IP ranges, routes and firewall rules may be affected by the gateway’s tunnels, and to evaluate the blast radius of any proposed changes to either resource. \ No newline at end of file +Each HA VPN Gateway is created inside a single VPC network. Linking the gateway to its `gcp-compute-network` allows Overmind to trace which IP ranges, routes and firewall rules may be affected by the gateway’s tunnels, and to evaluate the blast radius of any proposed changes to either resource. 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 index ea77ac05..00eb2786 100644 --- 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 @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/ **Terrafrom Mappings:** - * `google_compute_vpn_tunnel.name` +* `google_compute_vpn_tunnel.name` ## Supported Methods @@ -28,4 +28,4 @@ For dynamic (BGP) routing, a VPN tunnel is attached to a Cloud Router. The route ### [`gcp-compute-vpn-gateway`](/sources/gcp/Types/gcp-compute-vpn-gateway) -Every VPN tunnel is created on a specific VPN Gateway (Classic or HA). The gateway provides the Google Cloud termination point, while the tunnel specifies the individual encrypted session parameters. \ No newline at end of file +Every VPN tunnel is created on a specific VPN Gateway (Classic or HA). The gateway provides the Google Cloud termination point, while the tunnel specifies the individual encrypted session parameters. 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 index bfbf35de..3292ae54 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-cluster.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-cluster.md @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/kubernetes-engine/docs/concepts **Terrafrom Mappings:** - * `google_container_cluster.id` +* `google_container_cluster.id` ## Supported Methods @@ -52,4 +52,4 @@ GKE uses IAM service accounts for the control plane, node VMs and workload ident ### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) -Cluster audit logs, events or notifications can be exported to a Pub/Sub topic (e.g., via Log Sinks or Notification Channels). Any topic configured as a destination for the cluster is related here. \ No newline at end of file +Cluster audit logs, events or notifications can be exported to a Pub/Sub topic (e.g., via Log Sinks or Notification Channels). Any topic configured as a destination for the cluster is related here. 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 index 58eeb4a0..e2ad3b5b 100644 --- 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 @@ -9,7 +9,7 @@ Official documentation: https://cloud.google.com/kubernetes-engine/docs/concepts **Terrafrom Mappings:** - * `google_container_node_pool.id` +* `google_container_node_pool.id` ## Supported Methods @@ -45,4 +45,4 @@ A node pool is a child resource of a GKE cluster; this link identifies the paren ### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) -Each node runs with a Google service account that provides credentials for pulling container images, writing logs, and calling Google APIs. The pool stores a reference to that IAM Service Account. \ No newline at end of file +Each node runs with a Google service account that provides credentials for pulling container images, writing logs, and calling Google APIs. The pool stores a reference to that IAM Service Account. 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 index 6de737ca..ba4e7380 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataform-repository.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataform-repository.md @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/dataform/reference/rest **Terrafrom Mappings:** - * `google_dataform_repository.id` +* `google_dataform_repository.id` ## Supported Methods @@ -32,4 +32,4 @@ Dataform uses a service account to fetch code from remote Git repositories and t ### [`gcp-secret-manager-secret`](/sources/gcp/Types/gcp-secret-manager-secret) -When a repository is linked to an external Git provider, the authentication token is stored in Secret Manager. The field `authentication_token_secret_version` references the secret (and version) that holds the token. \ No newline at end of file +When a repository is linked to an external Git provider, the authentication token is stored in Secret Manager. The field `authentication_token_secret_version` references the secret (and version) that holds the token. 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 index 9fe13d0b..33a59939 100644 --- 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 @@ -8,10 +8,10 @@ For further details see the official API reference: https://cloud.google.com/dat **Terrafrom Mappings:** - * `google_dataplex_aspect_type.id` +* `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. \ No newline at end of file +* `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 index 4cd51511..036a3d1a 100644 --- 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 @@ -8,7 +8,7 @@ For full details see the official REST reference: https://cloud.google.com/datap **Terrafrom Mappings:** - * `google_dataplex_datascan.id` +* `google_dataplex_datascan.id` ## Supported Methods @@ -24,4 +24,4 @@ A Dataplex Data Scan may target a BigQuery table as its data source; linking the ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -When the data asset under review is a set of files stored in Cloud Storage, Dataplex references the underlying bucket. Linking the scan to the bucket reveals how changes to bucket configuration or contents could influence upcoming scan results. \ No newline at end of file +When the data asset under review is a set of files stored in Cloud Storage, Dataplex references the underlying bucket. Linking the scan to the bucket reveals how changes to bucket configuration or contents could influence upcoming scan results. 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 index 8a40aa47..8c0ff895 100644 --- 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 @@ -8,10 +8,10 @@ Official documentation: https://cloud.google.com/data-catalog/docs/reference/res **Terrafrom Mappings:** - * `google_dataplex_entry_group.id` +* `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. \ No newline at end of file +* `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 index 1719da66..fa25c6b7 100644 --- 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 @@ -8,10 +8,10 @@ For a full description of each field and the underlying API, see the official Go **Terraform Mappings:** - * `google_dataproc_autoscaling_policy.name` +* `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`~~ \ No newline at end of file +* ~~`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 index 3de7966b..c78f2bf8 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-cluster.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-cluster.md @@ -7,7 +7,7 @@ A Google Cloud Dataproc Cluster is a managed group of Compute Engine virtual mac **Terrafrom Mappings:** - * `google_dataproc_cluster.name` +* `google_dataproc_cluster.name` ## Supported Methods @@ -63,4 +63,4 @@ The VMs within the cluster run under one or more IAM Service Accounts that grant ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -During creation, the cluster specifies Cloud Storage buckets for staging, temp and log output, making those buckets upstream dependencies. \ No newline at end of file +During creation, the cluster specifies Cloud Storage buckets for staging, temp and log output, making those buckets upstream dependencies. 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 index d574e644..fa9ffaa9 100644 --- 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 @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/dns/docs/zones **Terrafrom Mappings:** - * `google_dns_managed_zone.name` +* `google_dns_managed_zone.name` ## Supported Methods @@ -24,4 +24,4 @@ Private managed zones are explicitly linked to one or more VPC networks. The ass ### [`gcp-container-cluster`](/sources/gcp/Types/gcp-container-cluster) -GKE clusters frequently create or rely on Cloud DNS managed zones for service discovery and in-cluster load-balancing (e.g., when CloudDNS for Service Directory is enabled). Mapping a cluster to its managed zones reveals dependencies that affect name resolution, cross-cluster communication and potential namespace conflicts. \ No newline at end of file +GKE clusters frequently create or rely on Cloud DNS managed zones for service discovery and in-cluster load-balancing (e.g., when CloudDNS for Service Directory is enabled). Mapping a cluster to its managed zones reveals dependencies that affect name resolution, cross-cluster communication and potential namespace conflicts. 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 index e8af18e7..43b35590 100644 --- 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 @@ -8,10 +8,10 @@ For further details, refer to the official Google Cloud documentation: https://c **Terrafrom Mappings:** - * `google_essential_contacts_contact.id` +* `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]". \ No newline at end of file +* `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 index 48e4de96..ba63d761 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-file-instance.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-file-instance.md @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/filestore/docs/overview **Terrafrom Mappings:** - * `google_filestore_instance.id` +* `google_filestore_instance.id` ## Supported Methods @@ -24,4 +24,4 @@ A Filestore instance can be encrypted with a customer-managed Cloud KMS key (CME ### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) -Filestore instances are deployed into and reachable through a specific VPC network. This link identifies the Compute Network whose subnet provides the private IP addresses through which clients access the file share. \ No newline at end of file +Filestore instances are deployed into and reachable through a specific VPC network. This link identifies the Compute Network whose subnet provides the private IP addresses through which clients access the file share. 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 index e5f57580..7f183ee0 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-role.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-role.md @@ -9,4 +9,4 @@ A **Google Cloud IAM Role** is a logical grouping of one or more IAM permissions * `GET`: Get a gcp-iam-role by its "name" * `LIST`: List all gcp-iam-role -* ~~`SEARCH`~~ \ No newline at end of file +* ~~`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 index 9adfdb13..c8da0dd7 100644 --- 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 @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/iam/docs/creating-managing-serv **Terrafrom Mappings:** - * `google_service_account_key.id` +* `google_service_account_key.id` ## Supported Methods @@ -20,4 +20,4 @@ Official documentation: https://cloud.google.com/iam/docs/creating-managing-serv ### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) -A Service Account Key is always subordinate to, and uniquely associated with, a single IAM service account. Overmind links the key back to its parent service account so you can trace which workload the key belongs to, understand the permissions it inherits, and assess the blast radius should the key be compromised. \ No newline at end of file +A Service Account Key is always subordinate to, and uniquely associated with, a single IAM service account. Overmind links the key back to its parent service account so you can trace which workload the key belongs to, understand the permissions it inherits, and assess the blast radius should the key be 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 index f5868dc0..aada9443 100644 --- 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 @@ -8,8 +8,8 @@ For full details see the official documentation: https://cloud.google.com/iam/do **Terrafrom Mappings:** - * `google_service_account.email` - * `google_service_account.unique_id` +* `google_service_account.email` +* `google_service_account.unique_id` ## Supported Methods @@ -25,4 +25,4 @@ Every service account is created inside a single Cloud Resource Manager project. ### [`gcp-iam-service-account-key`](/sources/gcp/Types/gcp-iam-service-account-key) -Service account keys are cryptographic credentials associated with a service account. This link lists all keys (active, disabled or expired) that belong to the current service account, allowing you to audit key rotation and exposure risks. \ No newline at end of file +Service account keys are cryptographic credentials associated with a service account. This link lists all keys (active, disabled or expired) that belong to the current service account, allowing you to audit key rotation and exposure risks. 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 index fb4fc7b6..f7b3702d 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-bucket.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-bucket.md @@ -6,7 +6,6 @@ sidebar_label: gcp-logging-bucket A GCP Logging Bucket is a regional or multi-regional storage container managed by Cloud Logging that stores log entries routed from one or more Google Cloud projects, folders or organisations. Buckets provide fine-grained control over where logs are kept, how long they are retained, and which encryption keys protect them. Log buckets behave similarly to Cloud Storage buckets, but are optimised for log data and are accessed through the Cloud Logging API rather than through Cloud Storage. See the official documentation for full details: https://cloud.google.com/logging/docs/storage - ## Supported Methods * `GET`: Get a gcp-logging-bucket by its "locations|buckets" @@ -25,4 +24,4 @@ If CMEK is active, the bucket also keeps track of the specific key version that ### [`gcp-iam-service-account`](/sources/gcp/Types/gcp-iam-service-account) -Cloud Logging uses service accounts to write, read or route logs into a bucket. The bucket’s IAM policy may grant `roles/logging.bucketWriter` or `roles/logging.viewer` to particular service accounts, and the Log Router’s reserved service account must have permission to encrypt data when CMEK is enabled. \ No newline at end of file +Cloud Logging uses service accounts to write, read or route logs into a bucket. The bucket’s IAM policy may grant `roles/logging.bucketWriter` or `roles/logging.viewer` to particular service accounts, and the Log Router’s reserved service account must have permission to encrypt data when CMEK is enabled. 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 index d1638e90..0b3acdc9 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-link.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-link.md @@ -10,7 +10,6 @@ A GCP Logging Link is a Cloud Logging resource that continuously streams the log and each link specifies the destination BigQuery dataset, IAM writer identity, and lifecycle state. For further details see Google’s 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" @@ -25,4 +24,4 @@ A logging link targets exactly one BigQuery dataset; Overmind establishes this e ### [`gcp-logging-bucket`](/sources/gcp/Types/gcp-logging-bucket) -The logging link is defined inside a specific Log Bucket; this relationship lets you see which buckets are sending their logs onwards and to which destinations. \ No newline at end of file +The logging link is defined inside a specific Log Bucket; this relationship lets you see which buckets are sending their logs onwards and to which destinations. 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 index 9805d0a0..4176d787 100644 --- 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 @@ -6,7 +6,6 @@ sidebar_label: gcp-logging-saved-query A GCP Logging Saved Query is a reusable, named log query that is stored in Google Cloud Logging’s Logs Explorer. It contains the filter expression (or Log Query Language statement), any configured time-range presets and display options, allowing teams to quickly rerun common searches, share queries across projects, and use them as the basis for dashboards, log-based metrics or alerting policies. Because Saved Queries are resources in their own right, they can be created, read, updated and deleted through the Cloud Logging API, and are uniquely identified by the combination of the Google Cloud location and the query name. Official documentation: https://cloud.google.com/logging/docs/view/building-queries - ## Supported Methods * `GET`: Get a gcp-logging-saved-query by its "locations|savedQueries" 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 index b0fe6cad..e120fc1e 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-sink.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-sink.md @@ -32,4 +32,4 @@ When the destination is Pub/Sub, the sink exports each matching log entry as a m ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -For archival purposes a sink may export logs to a Cloud Storage bucket. The bucket must exist and grant the sink’s writer service account permission to create objects, making the storage bucket a direct dependency of the sink. \ No newline at end of file +For archival purposes a sink may export logs to a Cloud Storage bucket. The bucket must exist and grant the sink’s writer service account permission to create objects, making the storage bucket a direct dependency of the sink. 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 index 22acddea..24628aec 100644 --- 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 @@ -7,7 +7,7 @@ A Google Cloud Monitoring Alert Policy is a configuration object that defines th **Terrafrom Mappings:** - * `google_monitoring_alert_policy.id` +* `google_monitoring_alert_policy.id` ## Supported Methods @@ -19,4 +19,4 @@ A Google Cloud Monitoring Alert Policy is a configuration object that defines th ### [`gcp-monitoring-notification-channel`](/sources/gcp/Types/gcp-monitoring-notification-channel) -An alert policy can reference one or more notification channels so that, when its conditions are met, Cloud Monitoring sends notifications (e-mails, webhooks, SMS, etc.) through the linked gcp-monitoring-notification-channels. \ No newline at end of file +An alert policy can reference one or more notification channels so that, when its conditions are met, Cloud Monitoring sends notifications (e-mails, webhooks, SMS, etc.) through the linked gcp-monitoring-notification-channels. 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 index 65986d2b..be3446f8 100644 --- 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 @@ -8,10 +8,10 @@ For full details, see the official documentation: https://cloud.google.com/monit **Terrafrom Mappings:** - * `google_monitoring_dashboard.id` +* `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. \ No newline at end of file +* `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 index 8b181969..2f1545f9 100644 --- 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 @@ -7,7 +7,7 @@ A **Google Cloud Monitoring Notification Channel** specifies where and how Cloud **Terrafrom Mappings:** - * `google_monitoring_notification_channel.name` +* `google_monitoring_notification_channel.name` ## Supported Methods @@ -19,4 +19,4 @@ A **Google Cloud Monitoring Notification Channel** specifies where and how Cloud ### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) -If the notification channel’s `type` is `pubsub`, the channel references a specific Cloud Pub/Sub topic where alert messages are published. Overmind therefore links the notification channel to the corresponding `gcp-pub-sub-topic` resource so that you can trace how alerts propagate into event-driven workflows or downstream systems. \ No newline at end of file +If the notification channel’s `type` is `pubsub`, the channel references a specific Cloud Pub/Sub topic where alert messages are published. Overmind therefore links the notification channel to the corresponding `gcp-pub-sub-topic` resource so that you can trace how alerts propagate into event-driven workflows or downstream systems. 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 index 109bff0d..991c60cc 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-orgpolicy-policy.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-orgpolicy-policy.md @@ -8,7 +8,7 @@ For full details see Google’s official documentation: https://cloud.google.com **Terrafrom Mappings:** - * `google_org_policy_policy.name` +* `google_org_policy_policy.name` ## Supported Methods @@ -20,4 +20,4 @@ For full details see Google’s official documentation: https://cloud.google.com ### [`gcp-cloud-resource-manager-project`](/sources/gcp/Types/gcp-cloud-resource-manager-project) -A project is one of the resource hierarchy levels to which an Organisation Policy can be attached. Each gcp-orgpolicy-policy documented here is therefore linked to the gcp-cloud-resource-manager-project that the policy governs. \ No newline at end of file +A project is one of the resource hierarchy levels to which an Organisation Policy can be attached. Each gcp-orgpolicy-policy documented here is therefore linked to the gcp-cloud-resource-manager-project that the policy governs. 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 index cbd41890..7569096c 100644 --- 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 @@ -7,10 +7,10 @@ A Google Cloud Pub/Sub subscription represents a stream of messages delivered fr **Terrafrom Mappings:** - * `google_pubsub_subscription.name` - * `google_pubsub_subscription_iam_binding.subscription` - * `google_pubsub_subscription_iam_member.subscription` - * `google_pubsub_subscription_iam_policy.subscription` +* `google_pubsub_subscription.name` +* `google_pubsub_subscription_iam_binding.subscription` +* `google_pubsub_subscription_iam_member.subscription` +* `google_pubsub_subscription_iam_policy.subscription` ## Supported Methods @@ -38,4 +38,4 @@ Every subscription is attached to exactly one topic. All messages published to t ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -Cloud Storage buckets can emit object-change notifications to a Pub/Sub topic. If the subscription listens to such a topic, it is indirectly linked to the bucket that generated the events, allowing you to trace the flow from storage changes to message consumption. \ No newline at end of file +Cloud Storage buckets can emit object-change notifications to a Pub/Sub topic. If the subscription listens to such a topic, it is indirectly linked to the bucket that generated the events, allowing you to trace the flow from storage changes to message consumption. 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 index a9762627..7d63dced 100644 --- 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 @@ -8,10 +8,10 @@ For comprehensive information, see the official documentation: https://cloud.goo **Terrafrom Mappings:** - * `google_pubsub_topic.name` - * `google_pubsub_topic_iam_binding.topic` - * `google_pubsub_topic_iam_member.topic` - * `google_pubsub_topic_iam_policy.topic` +* `google_pubsub_topic.name` +* `google_pubsub_topic_iam_binding.topic` +* `google_pubsub_topic_iam_member.topic` +* `google_pubsub_topic_iam_policy.topic` ## Supported Methods @@ -31,4 +31,4 @@ Access to publish or subscribe is controlled through IAM roles that are granted ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -Cloud Storage buckets can be configured to send change notifications to a Pub/Sub topic (for example, object create or delete events). In such configurations, the bucket acts as a publisher, and the topic appears as a dependent destination for bucket event notifications. \ No newline at end of file +Cloud Storage buckets can be configured to send change notifications to a Pub/Sub topic (for example, object create or delete events). In such configurations, the bucket acts as a publisher, and the topic appears as a dependent destination for bucket event notifications. 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 index 38c08485..d9da7a85 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-redis-instance.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-redis-instance.md @@ -7,7 +7,7 @@ A GCP Redis Instance is a fully managed, in-memory data store provided by Cloud **Terrafrom Mappings:** - * `google_redis_instance.id` +* `google_redis_instance.id` ## Supported Methods @@ -27,4 +27,4 @@ A Redis instance is deployed inside a specific VPC network and is reachable only ### [`gcp-compute-ssl-certificate`](/sources/gcp/Types/gcp-compute-ssl-certificate) -When TLS is enabled for a Redis instance, it can reference a Compute Engine SSL certificate resource to present during encrypted client connections. The `gcp-compute-ssl-certificate` therefore represents the server certificate used to secure traffic to the Redis instance. \ No newline at end of file +When TLS is enabled for a Redis instance, it can reference a Compute Engine SSL certificate resource to present during encrypted client connections. The `gcp-compute-ssl-certificate` therefore represents the server certificate used to secure traffic to the Redis instance. 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 index 0fef4fe1..8d483720 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-revision.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-revision.md @@ -6,7 +6,6 @@ sidebar_label: gcp-run-revision A Cloud Run **Revision** is an immutable snapshot of a Cloud Run Service configuration at a particular point in time. Each time you deploy new code or change configuration, Cloud Run automatically creates a new revision and routes traffic according to your settings. A revision defines the container image to run, environment variables, resource limits, networking options, service account, secret mounts and more. Once created, a revision can never be modified – you can only create a new one. Official documentation: https://cloud.google.com/run/docs/reference/rest/v1/namespaces.revisions - ## Supported Methods * `GET`: Get a gcp-run-revision by its "locations|services|revisions" @@ -49,4 +48,4 @@ If the revision defines Cloud SQL connections, it will list one or more Cloud SQ ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -A revision may read from or write to Cloud Storage buckets (for example for static assets or generated files) when granted the appropriate IAM permissions, creating a potential dependency on those buckets. \ No newline at end of file +A revision may read from or write to Cloud Storage buckets (for example for static assets or generated files) when granted the appropriate IAM permissions, creating a potential dependency on those buckets. 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 index 501c218b..c0db35f3 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-service.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-service.md @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/run/docs **Terrafrom Mappings:** - * `google_cloud_run_v2_service.id` +* `google_cloud_run_v2_service.id` ## Supported Methods @@ -52,4 +52,4 @@ If Cloud SQL connections are configured via the Cloud SQL Auth Proxy side-car or ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -The Service may access files in Cloud Storage for static assets or as mounted volumes (Cloud Run volumes). Buckets listed here are those explicitly referenced by environment variables, IAM permissions or volume mounts. \ No newline at end of file +The Service may access files in Cloud Storage for static assets or as mounted volumes (Cloud Run volumes). Buckets listed here are those explicitly referenced by environment variables, IAM permissions or volume mounts. 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 index 13c17791..cb42e674 100644 --- 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 @@ -8,7 +8,7 @@ For full details see the official documentation: https://cloud.google.com/secret **Terrafrom Mappings:** - * `google_secret_manager_secret.secret_id` +* `google_secret_manager_secret.secret_id` ## Supported Methods @@ -24,4 +24,4 @@ If a Secret is configured to use customer-managed encryption (CMEK), it referenc ### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) -Secret Manager can publish events—such as the creation of a new Secret Version—to a Pub/Sub topic. This enables automated workflows like triggering Cloud Functions for secret rotation or auditing. The Secret therefore holds an optional link to any Pub/Sub topic configured for such notifications. \ No newline at end of file +Secret Manager can publish events—such as the creation of a new Secret Version—to a Pub/Sub topic. This enables automated workflows like triggering Cloud Functions for secret rotation or auditing. The Secret therefore holds an optional link to any Pub/Sub topic configured for such notifications. 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 index 55802996..0337b436 100644 --- 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 @@ -7,7 +7,6 @@ The **Security Center Service** resource represents the configuration of Securit Each instance of this resource indicates that SCC is running in the specified region and records the service‐wide settings that govern how findings are ingested, stored and surfaced. Official documentation: https://cloud.google.com/security-command-center/docs/reference/security-center-management/rest/v1/projects.locations.securityCenterServices/list - ## Supported Methods * `GET`: Get a gcp-security-center-management-security-center-service by its "locations|securityCenterServices" @@ -18,4 +17,4 @@ Official documentation: https://cloud.google.com/security-command-center/docs/re ### [`gcp-cloud-resource-manager-project`](/sources/gcp/Types/gcp-cloud-resource-manager-project) -A Security Center Service exists **inside** a specific Google Cloud project – the project determines billing, IAM policies and the scope of resources that SCC monitors. The Overmind link lets you pivot from the project to every Security Center Service it has enabled (and vice-versa), helping you see which projects have security monitoring active in each region. \ No newline at end of file +A Security Center Service exists **inside** a specific Google Cloud project – the project determines billing, IAM policies and the scope of resources that SCC monitors. The Overmind link lets you pivot from the project to every Security Center Service it has enabled (and vice-versa), helping you see which projects have security monitoring active in each region. 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 index 15d5ef4d..280899ba 100644 --- 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 @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/service-directory/docs/referenc **Terrafrom Mappings:** - * `google_service_directory_endpoint.id` +* `google_service_directory_endpoint.id` ## Supported Methods @@ -20,4 +20,4 @@ Official documentation: https://cloud.google.com/service-directory/docs/referenc ### [`gcp-compute-network`](/sources/gcp/Types/gcp-compute-network) -A Service Directory endpoint’s address usually resides within a VPC network. Linking an endpoint to its `gcp-compute-network` resource lets you trace which network the IP belongs to, ensuring that connectivity policies (firewalls, routes, private service access, etc.) permit clients to reach the service before deployment. \ No newline at end of file +A Service Directory endpoint’s address usually resides within a VPC network. Linking an endpoint to its `gcp-compute-network` resource lets you trace which network the IP belongs to, ensuring that connectivity policies (firewalls, routes, private service access, etc.) permit clients to reach the service before deployment. 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 index c2830fbf..a9d5d664 100644 --- 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 @@ -6,7 +6,6 @@ sidebar_label: gcp-service-usage-service A **Service Usage Service** represents an individual Google-managed API or service (e.g. `compute.googleapis.com`, `pubsub.googleapis.com`) and its enablement state inside a single GCP project. By querying this resource you can determine whether a particular service is currently enabled, disabled, or in another transitional state for that project, which is critical for understanding if downstream resources can be created successfully. Official documentation: https://cloud.google.com/service-usage/docs/reference/rest/v1/services - ## Supported Methods * `GET`: Get a gcp-service-usage-service by its "name" @@ -21,4 +20,4 @@ Every Service Usage Service exists **within** a single Cloud Resource Manager pr ### [`gcp-pub-sub-topic`](/sources/gcp/Types/gcp-pub-sub-topic) -A Pub/Sub topic can only be created or used if the **`pubsub.googleapis.com`** Service Usage Service is enabled in the same project. Overmind links the topic back to its enabling service so you can quickly spot configuration drift or missing API enablement that would prevent deployment. \ No newline at end of file +A Pub/Sub topic can only be created or used if the **`pubsub.googleapis.com`** Service Usage Service is enabled in the same project. Overmind links the topic back to its enabling service so you can quickly spot configuration drift or missing API enablement that would prevent deployment. 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 index ba49a385..53a90810 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-database.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-database.md @@ -7,7 +7,7 @@ A GCP Spanner Database is a logically isolated collection of relational data tha **Terrafrom Mappings:** - * `google_spanner_database.name` +* `google_spanner_database.name` ## Supported Methods @@ -31,4 +31,4 @@ Spanner databases may reference one another through backups, clones or restores. ### [`gcp-spanner-instance`](/sources/gcp/Types/gcp-spanner-instance) -Every Spanner database belongs to a single Spanner instance. This link lets you traverse from the database to the parent instance to understand the compute resources, regional configuration and IAM policies that ultimately govern the database. \ No newline at end of file +Every Spanner database belongs to a single Spanner instance. This link lets you traverse from the database to the parent instance to understand the compute resources, regional configuration and IAM policies that ultimately govern the database. 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 index 8d9baff5..cf6337d0 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-instance.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-instance.md @@ -8,7 +8,7 @@ For full details see the official documentation: https://cloud.google.com/spanne **Terrafrom Mappings:** - * `google_spanner_instance.name` +* `google_spanner_instance.name` ## Supported Methods @@ -20,4 +20,4 @@ For full details see the official documentation: https://cloud.google.com/spanne ### [`gcp-spanner-database`](/sources/gcp/Types/gcp-spanner-database) -Each Cloud Spanner instance can contain multiple Cloud Spanner databases. The `gcp-spanner-database` resource is therefore a child of the `gcp-spanner-instance`; enumerating databases or assessing their risks starts with traversing from the parent instance to its associated databases. \ No newline at end of file +Each Cloud Spanner instance can contain multiple Cloud Spanner databases. The `gcp-spanner-database` resource is therefore a child of the `gcp-spanner-instance`; enumerating databases or assessing their risks starts with traversing from the parent instance to its associated databases. 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 index 03511392..88892ae6 100644 --- 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 @@ -24,4 +24,4 @@ The `encryptionInfo` block inside the backup run references the exact Cloud KMS ### [`gcp-sql-admin-instance`](/sources/gcp/Types/gcp-sql-admin-instance) -Every backup run belongs to a single Cloud SQL instance. This link connects the backup run to its parent instance so you can see which database the backup protects and assess the impact of restoring or deleting it. \ No newline at end of file +Every backup run belongs to a single Cloud SQL instance. This link connects the backup run to its parent instance so you can see which database the backup protects and assess the impact of restoring or deleting it. 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 index 13145280..40dbbf64 100644 --- 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 @@ -28,4 +28,4 @@ Although backups are stored out-of-band, they are associated with the same VPC n ### [`gcp-sql-admin-instance`](/sources/gcp/Types/gcp-sql-admin-instance) -Every backup is generated from, and can be restored to, a specific Cloud SQL instance. This link identifies the parent instance, allowing you to evaluate how instance configuration (e.g. region, database version) affects backup usability and risk. \ No newline at end of file +Every backup is generated from, and can be restored to, a specific Cloud SQL instance. This link identifies the parent instance, allowing you to evaluate how instance configuration (e.g. region, database version) affects backup usability and risk. 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 index 41cd6a4f..82d68edb 100644 --- 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 @@ -7,7 +7,7 @@ A Google Cloud SQL Admin Instance represents a fully-managed relational database **Terrafrom Mappings:** - * `google_sql_database_instance.name` +* `google_sql_database_instance.name` ## Supported Methods @@ -43,4 +43,4 @@ An instance can reference another instance as its read replica or as the source ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -Imports, exports and point-in-time backups can read from or write to Cloud Storage. The instance therefore maintains references to buckets used for these operations. \ No newline at end of file +Imports, exports and point-in-time backups can read from or write to Cloud Storage. The instance therefore maintains references to buckets used for these operations. diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket-iam-policy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket-iam-policy.md index 506c5c49..0dd44a59 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket-iam-policy.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket-iam-policy.md @@ -7,9 +7,9 @@ A **Storage Bucket IAM policy** defines who (principals) can perform which actio **Terrafrom Mappings:** - * `google_storage_bucket_iam_binding.bucket` - * `google_storage_bucket_iam_member.bucket` - * `google_storage_bucket_iam_policy.bucket` +* `google_storage_bucket_iam_binding.bucket` +* `google_storage_bucket_iam_member.bucket` +* `google_storage_bucket_iam_policy.bucket` ## Supported Methods @@ -33,4 +33,4 @@ Service accounts are common principals in bucket policies. Linking reveals which ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -The IAM policy is attached to and governs a specific Cloud Storage bucket; this link connects the policy object to the underlying bucket resource. \ No newline at end of file +The IAM policy is attached to and governs a specific Cloud Storage bucket; this link connects the policy object to the underlying bucket resource. 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 index 1bd3e48f..2e0ecf48 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket.md @@ -8,10 +8,10 @@ For full details see the official documentation: https://cloud.google.com/storag **Terrafrom Mappings:** - * `google_storage_bucket.name` - * `google_storage_bucket_iam_binding.bucket` - * `google_storage_bucket_iam_member.bucket` - * `google_storage_bucket_iam_policy.bucket` +* `google_storage_bucket.name` +* `google_storage_bucket_iam_binding.bucket` +* `google_storage_bucket_iam_member.bucket` +* `google_storage_bucket_iam_policy.bucket` ## Supported Methods @@ -35,4 +35,4 @@ Cloud Logging can route logs from a Logging Bucket to Cloud Storage for long-ter ### [`gcp-storage-bucket-iam-policy`](/sources/gcp/Types/gcp-storage-bucket-iam-policy) -Every Storage Bucket has an IAM policy that defines who can read, write or administer it. That policy is exposed as a separate `gcp-storage-bucket-iam-policy` object, which is directly attached to this bucket. \ No newline at end of file +Every Storage Bucket has an IAM policy that defines who can read, write or administer it. That policy is exposed as a separate `gcp-storage-bucket-iam-policy` object, which is directly attached to this bucket. 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 index c0836910..f0046429 100644 --- 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 @@ -8,7 +8,7 @@ Official documentation: https://cloud.google.com/storage-transfer/docs/create-tr **Terrafrom Mappings:** - * `google_storage_transfer_job.name` +* `google_storage_transfer_job.name` ## Supported Methods @@ -36,4 +36,4 @@ When transferring from external providers such as AWS S3 or Azure Blob Storage, ### [`gcp-storage-bucket`](/sources/gcp/Types/gcp-storage-bucket) -Every transfer job specifies at least one Cloud Storage bucket as a source and/or destination; therefore it has direct relationships to the buckets involved in the data copy. \ No newline at end of file +Every transfer job specifies at least one Cloud Storage bucket as a source and/or destination; therefore it has direct relationships to the buckets involved in the data copy. From 957e484812a933ff20bc77b89a15806982abb36a Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 27 Feb 2026 15:00:34 +0100 Subject: [PATCH 29/74] Review prompt and feedback (#4041) Add an optional `review_prompt` input to the code review workflow for custom prompts and structured, actionable feedback. ---

Open in Web Open in Cursor 

--- > [!NOTE] > **Low Risk** > Primarily CI workflow and documentation updates plus test-only refactors (Go `any`/pointer helper cleanup). Low production risk, but the workflow prompt handling could affect review output formatting if misconfigured. > > **Overview** > Adds an optional `review_prompt` input to the Cursor code review GitHub Action, selecting between a structured default prompt and a user-provided prompt, and passing the effective prompt into the `agent` invocation. > > Records the prompt source/effective prompt into `$GITHUB_ENV` (with a randomized heredoc delimiter) and includes both in the Actions step summary; docs are updated with the new input and CLI example. > > Refactors a few Go tests to use `any` and simplified pointer creation, removing local `stringPtr`/`newPtr` helpers (no functional changes to production code). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 13e5f33401380fd95d9c742ee12168d01ae21659. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: b2e3ef21991eae0ac58a3a9901e044698b2db503 --- ...esql-flexible-server-firewall-rule_test.go | 12 +++------ sources/azure/manual/keyvault-key_test.go | 26 +++++++++---------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/sources/azure/manual/dbforpostgresql-flexible-server-firewall-rule_test.go b/sources/azure/manual/dbforpostgresql-flexible-server-firewall-rule_test.go index 0240280f..58891de8 100644 --- a/sources/azure/manual/dbforpostgresql-flexible-server-firewall-rule_test.go +++ b/sources/azure/manual/dbforpostgresql-flexible-server-firewall-rule_test.go @@ -314,15 +314,11 @@ func TestDBforPostgreSQLFlexibleServerFirewallRule(t *testing.T) { func createAzurePostgreSQLFlexibleServerFirewallRule(serverName, firewallRuleName string) *armpostgresqlflexibleservers.FirewallRule { ruleID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/" + serverName + "/firewallRules/" + firewallRuleName return &armpostgresqlflexibleservers.FirewallRule{ - Name: stringPtr(firewallRuleName), - ID: stringPtr(ruleID), + Name: new(firewallRuleName), + ID: new(ruleID), Properties: &armpostgresqlflexibleservers.FirewallRuleProperties{ - StartIPAddress: stringPtr("0.0.0.0"), - EndIPAddress: stringPtr("255.255.255.255"), + StartIPAddress: new("0.0.0.0"), + EndIPAddress: new("255.255.255.255"), }, } } - -func stringPtr(s string) *string { - return &s -} diff --git a/sources/azure/manual/keyvault-key_test.go b/sources/azure/manual/keyvault-key_test.go index e989768e..92ec8cd4 100644 --- a/sources/azure/manual/keyvault-key_test.go +++ b/sources/azure/manual/keyvault-key_test.go @@ -22,8 +22,6 @@ import ( "github.com/overmindtech/cli/sources/stdlib" ) -func newPtr[T any](v T) *T { return &v } - type mockKeysPager struct { pages []armkeyvault.KeysClientListResponse index int @@ -236,7 +234,7 @@ func TestKeyVaultKey(t *testing.T) { testClient := &testKeysClient{ MockKeysClient: mockClient, - pager: mockPager, + pager: mockPager, } wrapper := manual.NewKeyVaultKey(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) @@ -310,7 +308,7 @@ func TestKeyVaultKey(t *testing.T) { testClient := &testKeysClient{ MockKeysClient: mockClient, - pager: mockPager, + pager: mockPager, } wrapper := manual.NewKeyVaultKey(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) @@ -361,7 +359,7 @@ func TestKeyVaultKey(t *testing.T) { testClient := &testKeysClient{ MockKeysClient: mockClient, - pager: errorPager, + pager: errorPager, } wrapper := manual.NewKeyVaultKey(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) @@ -477,25 +475,25 @@ func TestKeyVaultKey(t *testing.T) { func createAzureKey(keyName, subscriptionID, resourceGroup, vaultName string) *armkeyvault.Key { return &armkeyvault.Key{ - ID: newPtr(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.KeyVault/vaults/%s/keys/%s", subscriptionID, resourceGroup, vaultName, keyName)), - Name: newPtr(keyName), - Type: newPtr("Microsoft.KeyVault/vaults/keys"), + ID: new(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.KeyVault/vaults/%s/keys/%s", subscriptionID, resourceGroup, vaultName, keyName)), + Name: new(keyName), + Type: new("Microsoft.KeyVault/vaults/keys"), Tags: map[string]*string{ - "env": newPtr("test"), - "project": newPtr("testing"), + "env": new("test"), + "project": new("testing"), }, Properties: &armkeyvault.KeyProperties{ - KeyURI: newPtr(fmt.Sprintf("https://%s.vault.azure.net/keys/%s", vaultName, keyName)), + KeyURI: new(fmt.Sprintf("https://%s.vault.azure.net/keys/%s", vaultName, keyName)), }, } } func createAzureKeyMinimal(keyName string) *armkeyvault.Key { return &armkeyvault.Key{ - Name: newPtr(keyName), - Type: newPtr("Microsoft.KeyVault/vaults/keys"), + Name: new(keyName), + Type: new("Microsoft.KeyVault/vaults/keys"), Tags: map[string]*string{ - "env": newPtr("test"), + "env": new("test"), }, Properties: &armkeyvault.KeyProperties{}, } From c7d85405d754b86a694c2aa39e6a54ef111b1ca0 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 27 Feb 2026 15:20:25 +0100 Subject: [PATCH 30/74] CI updates and outage-tracker db test fix (#4045) > [!NOTE] > **Low Risk** > Low risk: changes are limited to CI/workflow action version pinning, a Terraform cache key fix, and test-only DB migration locking to reduce flakiness. > > **Overview** > **CI/workflow updates:** fixes the Terraform provider cache key to hash `cli/.terraform.lock.hcl` (instead of the repo-root lockfile), and pins previously unversioned GitHub Actions (`depot/*` and `cloudsmith-io/action`) to specific releases. > > **Outage tracker test stability:** wraps River schema migrations in `CreateTestPgPool` with a Postgres `pg_advisory_lock`/`unlock` to prevent concurrent migrations when tests run in parallel against the same database. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit cb80410d44baf85686bc3d461cbba3e84b4da530. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: b6f394e25717a6abb2bd042ca35866ee51d3c468 --- .github/workflows/docker-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 37c61f71..d83d75c8 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -35,12 +35,12 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: depot/use-action@v1 + - uses: depot/use-action@v1.3.1 with: project: xnsnw3m20t - name: Build and push container - uses: depot/build-push-action@v1 + uses: depot/build-push-action@v1.17.0 id: build with: project: xnsnw3m20t From c9d68073a88bfa65098d17ef435a4985040ad9f0 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:35:00 +0000 Subject: [PATCH 31/74] Implement Azure Storage Private Endpoint Connection Client (#4048) image --- > [!NOTE] > **Medium Risk** > Adds a new Azure discovery adapter and wiring that introduces additional Azure API calls and required permissions; mistakes could cause missing resources or incorrect linking across scopes. > > **Overview** > Adds first-class discovery for Azure Storage Account *private endpoint connections* via a new `StoragePrivateEndpointConnectionsClient` wrapper over the Azure SDK and a new searchable adapter (`NewStoragePrivateEndpointConnection`) supporting `Get`, `Search`, and streaming search. > > Wires the new adapter into `manual/adapters.go` (including Azure SDK client initialization) and updates resource-ID parsing (`GetResourceIDPathKeys`) so composite lookups can be derived from `.../storageAccounts/{account}/privateEndpointConnections/{name}`. > > Includes generated GoMock + unit tests validating lookup/key behavior, paging, error handling, health mapping from provisioning state, and links to the parent `StorageAccount` and referenced `NetworkPrivateEndpoint` (with cross-resource-group scope extraction). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit faba632ef6799dd11aaec8e56be25d667b06cf56. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 8c30ca5f080caedea989adbb0f51c0d0b594b6f4 --- ...rage-private-endpoint-connection-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + .../storage-private-endpoint-connection.go | 235 +++++++++++++ ...torage-private-endpoint-connection_test.go | 321 ++++++++++++++++++ ...rage_private_endpoint_connection_client.go | 72 ++++ sources/azure/shared/utils.go | 5 +- 6 files changed, 676 insertions(+), 2 deletions(-) create mode 100644 sources/azure/clients/storage-private-endpoint-connection-client.go create mode 100644 sources/azure/manual/storage-private-endpoint-connection.go create mode 100644 sources/azure/manual/storage-private-endpoint-connection_test.go create mode 100644 sources/azure/shared/mocks/mock_storage_private_endpoint_connection_client.go diff --git a/sources/azure/clients/storage-private-endpoint-connection-client.go b/sources/azure/clients/storage-private-endpoint-connection-client.go new file mode 100644 index 00000000..d591473a --- /dev/null +++ b/sources/azure/clients/storage-private-endpoint-connection-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" +) + +//go:generate mockgen -destination=../shared/mocks/mock_storage_private_endpoint_connection_client.go -package=mocks -source=storage-private-endpoint-connection-client.go + +// PrivateEndpointConnectionsPager is a type alias for the generic Pager interface with storage private endpoint connection list response type. +type PrivateEndpointConnectionsPager = Pager[armstorage.PrivateEndpointConnectionsClientListResponse] + +// StoragePrivateEndpointConnectionsClient is an interface for interacting with Azure storage account private endpoint connections. +type StoragePrivateEndpointConnectionsClient interface { + Get(ctx context.Context, resourceGroupName string, accountName string, privateEndpointConnectionName string) (armstorage.PrivateEndpointConnectionsClientGetResponse, error) + List(ctx context.Context, resourceGroupName string, accountName string) PrivateEndpointConnectionsPager +} + +type storagePrivateEndpointConnectionsClient struct { + client *armstorage.PrivateEndpointConnectionsClient +} + +func (c *storagePrivateEndpointConnectionsClient) Get(ctx context.Context, resourceGroupName string, accountName string, privateEndpointConnectionName string) (armstorage.PrivateEndpointConnectionsClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, accountName, privateEndpointConnectionName, nil) +} + +func (c *storagePrivateEndpointConnectionsClient) List(ctx context.Context, resourceGroupName string, accountName string) PrivateEndpointConnectionsPager { + return c.client.NewListPager(resourceGroupName, accountName, nil) +} + +// NewStoragePrivateEndpointConnectionsClient creates a new StoragePrivateEndpointConnectionsClient from the Azure SDK client. +func NewStoragePrivateEndpointConnectionsClient(client *armstorage.PrivateEndpointConnectionsClient) StoragePrivateEndpointConnectionsClient { + return &storagePrivateEndpointConnectionsClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 24d7ad41..b859cea7 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -111,6 +111,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create encryption scopes client: %w", err) } + privateEndpointConnectionsClient, err := armstorage.NewPrivateEndpointConnectionsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create private endpoint connections client: %w", err) + } + virtualNetworksClient, err := armnetwork.NewVirtualNetworksClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create virtual networks client: %w", err) @@ -364,6 +369,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewEncryptionScopesClient(encryptionScopesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewStoragePrivateEndpointConnection( + clients.NewStoragePrivateEndpointConnectionsClient(privateEndpointConnectionsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewNetworkVirtualNetwork( clients.NewVirtualNetworksClient(virtualNetworksClient), resourceGroupScopes, @@ -570,6 +579,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewStorageQueues(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewStorageTable(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewStorageEncryptionScope(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewStoragePrivateEndpointConnection(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkVirtualNetwork(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkSubnet(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkVirtualNetworkPeering(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/storage-private-endpoint-connection.go b/sources/azure/manual/storage-private-endpoint-connection.go new file mode 100644 index 00000000..2a474430 --- /dev/null +++ b/sources/azure/manual/storage-private-endpoint-connection.go @@ -0,0 +1,235 @@ +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" + "github.com/overmindtech/cli/sources/shared" +) + +var StoragePrivateEndpointConnectionLookupByName = shared.NewItemTypeLookup("name", azureshared.StoragePrivateEndpointConnection) + +type storagePrivateEndpointConnectionWrapper struct { + client clients.StoragePrivateEndpointConnectionsClient + + *azureshared.MultiResourceGroupBase +} + +// NewStoragePrivateEndpointConnection returns a SearchableWrapper for Azure storage account private endpoint connections. +func NewStoragePrivateEndpointConnection(client clients.StoragePrivateEndpointConnectionsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &storagePrivateEndpointConnectionWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_STORAGE, + azureshared.StoragePrivateEndpointConnection, + ), + } +} + +func (s storagePrivateEndpointConnectionWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: storageAccountName and privateEndpointConnectionName", + Scope: scope, + ItemType: s.Type(), + } + } + accountName := queryParts[0] + connectionName := queryParts[1] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + resp, err := s.client.Get(ctx, rgScope.ResourceGroup, accountName, connectionName) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + item, sdpErr := s.azurePrivateEndpointConnectionToSDPItem(&resp.PrivateEndpointConnection, accountName, connectionName, scope) + if sdpErr != nil { + return nil, sdpErr + } + return item, nil +} + +func (s storagePrivateEndpointConnectionWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + StorageAccountLookupByName, + StoragePrivateEndpointConnectionLookupByName, + } +} + +func (s storagePrivateEndpointConnectionWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: storageAccountName", + Scope: scope, + ItemType: s.Type(), + } + } + accountName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + pager := s.client.List(ctx, rgScope.ResourceGroup, accountName) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + for _, conn := range page.Value { + if conn.Name == nil { + continue + } + + item, sdpErr := s.azurePrivateEndpointConnectionToSDPItem(conn, accountName, *conn.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (s storagePrivateEndpointConnectionWrapper) 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 + } + accountName := 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, accountName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, conn := range page.Value { + if conn.Name == nil { + continue + } + item, sdpErr := s.azurePrivateEndpointConnectionToSDPItem(conn, accountName, *conn.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (s storagePrivateEndpointConnectionWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + StorageAccountLookupByName, + }, + } +} + +func (s storagePrivateEndpointConnectionWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.StorageAccount: true, + azureshared.NetworkPrivateEndpoint: true, + } +} + +func (s storagePrivateEndpointConnectionWrapper) azurePrivateEndpointConnectionToSDPItem(conn *armstorage.PrivateEndpointConnection, accountName, connectionName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(conn) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(accountName, connectionName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.StoragePrivateEndpointConnection.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + } + + // Health from provisioning state + if conn.Properties != nil && conn.Properties.ProvisioningState != nil { + switch *conn.Properties.ProvisioningState { + case armstorage.PrivateEndpointConnectionProvisioningStateSucceeded: + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case armstorage.PrivateEndpointConnectionProvisioningStateCreating, + armstorage.PrivateEndpointConnectionProvisioningStateDeleting: + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case armstorage.PrivateEndpointConnectionProvisioningStateFailed: + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + // Link to parent Storage Account + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageAccount.String(), + Method: sdp.QueryMethod_GET, + Query: accountName, + Scope: scope, + }, + }) + + // Link to Network Private Endpoint when present (may be in different resource group) + if conn.Properties != nil && conn.Properties.PrivateEndpoint != nil && conn.Properties.PrivateEndpoint.ID != nil { + peID := *conn.Properties.PrivateEndpoint.ID + peName := azureshared.ExtractResourceName(peID) + if peName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(peID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPrivateEndpoint.String(), + Method: sdp.QueryMethod_GET, + Query: peName, + Scope: linkedScope, + }, + }) + } + } + + return sdpItem, nil +} + +func (s storagePrivateEndpointConnectionWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Storage/storageAccounts/privateEndpointConnections/read", + } +} + +func (s storagePrivateEndpointConnectionWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/storage-private-endpoint-connection_test.go b/sources/azure/manual/storage-private-endpoint-connection_test.go new file mode 100644 index 00000000..8bb2d545 --- /dev/null +++ b/sources/azure/manual/storage-private-endpoint-connection_test.go @@ -0,0 +1,321 @@ +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/storage/armstorage/v3" + "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" +) + +type mockPrivateEndpointConnectionsPager struct { + pages []armstorage.PrivateEndpointConnectionsClientListResponse + index int +} + +func (m *mockPrivateEndpointConnectionsPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockPrivateEndpointConnectionsPager) NextPage(ctx context.Context) (armstorage.PrivateEndpointConnectionsClientListResponse, error) { + if m.index >= len(m.pages) { + return armstorage.PrivateEndpointConnectionsClientListResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type testStoragePrivateEndpointConnectionsClient struct { + *mocks.MockStoragePrivateEndpointConnectionsClient + pager clients.PrivateEndpointConnectionsPager +} + +func (t *testStoragePrivateEndpointConnectionsClient) List(ctx context.Context, resourceGroupName, accountName string) clients.PrivateEndpointConnectionsPager { + return t.pager +} + +func TestStoragePrivateEndpointConnection(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + accountName := "teststorageaccount" + connectionName := "test-pec" + + t.Run("Get", func(t *testing.T) { + conn := createAzureStoragePrivateEndpointConnection(connectionName, "") + + mockClient := mocks.NewMockStoragePrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, accountName, connectionName).Return( + armstorage.PrivateEndpointConnectionsClientGetResponse{ + PrivateEndpointConnection: *conn, + }, nil) + + testClient := &testStoragePrivateEndpointConnectionsClient{MockStoragePrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewStoragePrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(accountName, connectionName) + 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.StoragePrivateEndpointConnection.String() { + t.Errorf("Expected type %s, got %s", azureshared.StoragePrivateEndpointConnection, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != shared.CompositeLookupKey(accountName, connectionName) { + t.Errorf("Expected unique attribute value %s, got %s", shared.CompositeLookupKey(accountName, connectionName), sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + linkedQueries := sdpItem.GetLinkedItemQueries() + if len(linkedQueries) < 1 { + t.Fatalf("Expected at least 1 linked query, got: %d", len(linkedQueries)) + } + + foundStorageAccount := false + for _, lq := range linkedQueries { + if lq.GetQuery().GetType() == azureshared.StorageAccount.String() { + foundStorageAccount = true + if lq.GetQuery().GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected StorageAccount link method GET, got %v", lq.GetQuery().GetMethod()) + } + if lq.GetQuery().GetQuery() != accountName { + t.Errorf("Expected StorageAccount query %s, got %s", accountName, lq.GetQuery().GetQuery()) + } + } + } + if !foundStorageAccount { + t.Error("Expected linked query to StorageAccount") + } + }) + }) + + t.Run("Get_WithPrivateEndpointLink", func(t *testing.T) { + peID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-pe" + conn := createAzureStoragePrivateEndpointConnection(connectionName, peID) + + mockClient := mocks.NewMockStoragePrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, accountName, connectionName).Return( + armstorage.PrivateEndpointConnectionsClientGetResponse{ + PrivateEndpointConnection: *conn, + }, nil) + + testClient := &testStoragePrivateEndpointConnectionsClient{MockStoragePrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewStoragePrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(accountName, connectionName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + foundPrivateEndpoint := false + for _, lq := range sdpItem.GetLinkedItemQueries() { + if lq.GetQuery().GetType() == azureshared.NetworkPrivateEndpoint.String() { + foundPrivateEndpoint = true + if lq.GetQuery().GetQuery() != "test-pe" { + t.Errorf("Expected NetworkPrivateEndpoint query 'test-pe', got %s", lq.GetQuery().GetQuery()) + } + break + } + } + if !foundPrivateEndpoint { + t.Error("Expected linked query to NetworkPrivateEndpoint when PrivateEndpoint ID is set") + } + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockStoragePrivateEndpointConnectionsClient(ctrl) + testClient := &testStoragePrivateEndpointConnectionsClient{MockStoragePrivateEndpointConnectionsClient: mockClient} + + wrapper := manual.NewStoragePrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], accountName, true) + if qErr == nil { + t.Error("Expected error when providing insufficient query parts, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + conn1 := createAzureStoragePrivateEndpointConnection("pec-1", "") + conn2 := createAzureStoragePrivateEndpointConnection("pec-2", "") + + mockClient := mocks.NewMockStoragePrivateEndpointConnectionsClient(ctrl) + mockPager := &mockPrivateEndpointConnectionsPager{ + pages: []armstorage.PrivateEndpointConnectionsClientListResponse{ + { + PrivateEndpointConnectionListResult: armstorage.PrivateEndpointConnectionListResult{ + Value: []*armstorage.PrivateEndpointConnection{conn1, conn2}, + }, + }, + }, + } + + testClient := &testStoragePrivateEndpointConnectionsClient{ + MockStoragePrivateEndpointConnectionsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewStoragePrivateEndpointConnection(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, wrapper.Scopes()[0], accountName, 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.Fatalf("Expected no validation error, got: %v", err) + } + if item.GetType() != azureshared.StoragePrivateEndpointConnection.String() { + t.Errorf("Expected type %s, got %s", azureshared.StoragePrivateEndpointConnection, item.GetType()) + } + } + }) + + t.Run("Search_NilNameSkipped", func(t *testing.T) { + validConn := createAzureStoragePrivateEndpointConnection("valid-pec", "") + + mockClient := mocks.NewMockStoragePrivateEndpointConnectionsClient(ctrl) + mockPager := &mockPrivateEndpointConnectionsPager{ + pages: []armstorage.PrivateEndpointConnectionsClientListResponse{ + { + PrivateEndpointConnectionListResult: armstorage.PrivateEndpointConnectionListResult{ + Value: []*armstorage.PrivateEndpointConnection{ + {Name: nil}, + validConn, + }, + }, + }, + }, + } + + testClient := &testStoragePrivateEndpointConnectionsClient{ + MockStoragePrivateEndpointConnectionsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewStoragePrivateEndpointConnection(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, wrapper.Scopes()[0], accountName, 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)) + } + if sdpItems[0].UniqueAttributeValue() != shared.CompositeLookupKey(accountName, "valid-pec") { + t.Errorf("Expected unique value %s, got %s", shared.CompositeLookupKey(accountName, "valid-pec"), sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockStoragePrivateEndpointConnectionsClient(ctrl) + testClient := &testStoragePrivateEndpointConnectionsClient{MockStoragePrivateEndpointConnectionsClient: mockClient} + + wrapper := manual.NewStoragePrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("private endpoint connection not found") + + mockClient := mocks.NewMockStoragePrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, accountName, "nonexistent-pec").Return( + armstorage.PrivateEndpointConnectionsClientGetResponse{}, expectedErr) + + testClient := &testStoragePrivateEndpointConnectionsClient{MockStoragePrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewStoragePrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(accountName, "nonexistent-pec") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent private endpoint connection, but got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + wrapper := manual.NewStoragePrivateEndpointConnection(nil, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + links := wrapper.PotentialLinks() + if !links[azureshared.StorageAccount] { + t.Error("Expected StorageAccount in PotentialLinks") + } + if !links[azureshared.NetworkPrivateEndpoint] { + t.Error("Expected NetworkPrivateEndpoint in PotentialLinks") + } + }) +} + +func createAzureStoragePrivateEndpointConnection(connectionName, privateEndpointID string) *armstorage.PrivateEndpointConnection { + conn := &armstorage.PrivateEndpointConnection{ + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Storage/storageAccounts/teststorageaccount/privateEndpointConnections/" + connectionName), + Name: new(connectionName), + Type: new("Microsoft.Storage/storageAccounts/privateEndpointConnections"), + Properties: &armstorage.PrivateEndpointConnectionProperties{ + ProvisioningState: to.Ptr(armstorage.PrivateEndpointConnectionProvisioningStateSucceeded), + PrivateLinkServiceConnectionState: &armstorage.PrivateLinkServiceConnectionState{ + Status: to.Ptr(armstorage.PrivateEndpointServiceConnectionStatusApproved), + }, + }, + } + if privateEndpointID != "" { + conn.Properties.PrivateEndpoint = &armstorage.PrivateEndpoint{ + ID: new(privateEndpointID), + } + } + return conn +} diff --git a/sources/azure/shared/mocks/mock_storage_private_endpoint_connection_client.go b/sources/azure/shared/mocks/mock_storage_private_endpoint_connection_client.go new file mode 100644 index 00000000..7fce19df --- /dev/null +++ b/sources/azure/shared/mocks/mock_storage_private_endpoint_connection_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: storage-private-endpoint-connection-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_storage_private_endpoint_connection_client.go -package=mocks -source=storage-private-endpoint-connection-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armstorage "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockStoragePrivateEndpointConnectionsClient is a mock of StoragePrivateEndpointConnectionsClient interface. +type MockStoragePrivateEndpointConnectionsClient struct { + ctrl *gomock.Controller + recorder *MockStoragePrivateEndpointConnectionsClientMockRecorder + isgomock struct{} +} + +// MockStoragePrivateEndpointConnectionsClientMockRecorder is the mock recorder for MockStoragePrivateEndpointConnectionsClient. +type MockStoragePrivateEndpointConnectionsClientMockRecorder struct { + mock *MockStoragePrivateEndpointConnectionsClient +} + +// NewMockStoragePrivateEndpointConnectionsClient creates a new mock instance. +func NewMockStoragePrivateEndpointConnectionsClient(ctrl *gomock.Controller) *MockStoragePrivateEndpointConnectionsClient { + mock := &MockStoragePrivateEndpointConnectionsClient{ctrl: ctrl} + mock.recorder = &MockStoragePrivateEndpointConnectionsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStoragePrivateEndpointConnectionsClient) EXPECT() *MockStoragePrivateEndpointConnectionsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockStoragePrivateEndpointConnectionsClient) Get(ctx context.Context, resourceGroupName, accountName, privateEndpointConnectionName string) (armstorage.PrivateEndpointConnectionsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, accountName, privateEndpointConnectionName) + ret0, _ := ret[0].(armstorage.PrivateEndpointConnectionsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockStoragePrivateEndpointConnectionsClientMockRecorder) Get(ctx, resourceGroupName, accountName, privateEndpointConnectionName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStoragePrivateEndpointConnectionsClient)(nil).Get), ctx, resourceGroupName, accountName, privateEndpointConnectionName) +} + +// List mocks base method. +func (m *MockStoragePrivateEndpointConnectionsClient) List(ctx context.Context, resourceGroupName, accountName string) clients.PrivateEndpointConnectionsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, resourceGroupName, accountName) + ret0, _ := ret[0].(clients.PrivateEndpointConnectionsPager) + return ret0 +} + +// List indicates an expected call of List. +func (mr *MockStoragePrivateEndpointConnectionsClientMockRecorder) List(ctx, resourceGroupName, accountName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockStoragePrivateEndpointConnectionsClient)(nil).List), ctx, resourceGroupName, accountName) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index c479dba0..09a64ed1 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -21,8 +21,9 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-storage-queue": {"storageAccounts", "queues"}, "azure-storage-blob-container": {"storageAccounts", "containers"}, "azure-storage-encryption-scope": {"storageAccounts", "encryptionScopes"}, - "azure-storage-file-share": {"storageAccounts", "shares"}, - "azure-storage-table": {"storageAccounts", "tables"}, + "azure-storage-file-share": {"storageAccounts", "shares"}, + "azure-storage-storage-account-private-endpoint-connection": {"storageAccounts", "privateEndpointConnections"}, + "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-sql-server-firewall-rule": {"servers", "firewallRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/firewallRules/{ruleName}", "azure-sql-server-virtual-network-rule": {"servers", "virtualNetworkRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/virtualNetworkRules/{ruleName}", From a013ab93227b84e8b01a9e54e7b4b396f3bbe697 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Mon, 2 Mar 2026 09:51:59 +0100 Subject: [PATCH 32/74] Source image promotion (#4046) Implement Source Container Image Versioning in the srcman controller to enable staged rollout of source images via Kargo promotion. --- Linear Issue: [ENG-2843](https://linear.app/overmind/issue/ENG-2843/source-container-image-versioning)

Open in Web Open in Cursor 

--- > [!NOTE] > **Medium Risk** > Changes how Source deployments choose container images and adds a ConfigMap watch that can trigger reconciles for all Sources, so a bad ConfigMap entry could roll out incorrect images broadly. > > **Overview** > Adds **ConfigMap-driven source image promotion**: `srcman` now optionally reads per-source-type image overrides from a `source-images` ConfigMap (keyed by `Source.spec.type`) and prefers these over `Source.spec.image` when reconciling Deployments. > > Wires this through a new `--source-images-configmap`/`SOURCE_IMAGES_CONFIGMAP` setting, watches the configured ConfigMap to enqueue reconciles for all Sources on updates, and updates the app-of-apps kustomize base/overlays to create and patch the `source-images` ConfigMap. Includes new controller tests covering override, fallback, and update behavior. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f8c29c8cd00f1e7d585c2a5a49806089d17e7c01. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 0f9fbcf88977fdbffd22d58dd55b3e33775e77fc --- sources/azure/manual/keyvault-key_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/sources/azure/manual/keyvault-key_test.go b/sources/azure/manual/keyvault-key_test.go index 92ec8cd4..7c5462e2 100644 --- a/sources/azure/manual/keyvault-key_test.go +++ b/sources/azure/manual/keyvault-key_test.go @@ -22,6 +22,7 @@ import ( "github.com/overmindtech/cli/sources/stdlib" ) + type mockKeysPager struct { pages []armkeyvault.KeysClientListResponse index int From c50fbf614b273743334c32e37592e2a46ffe3fd7 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Mon, 2 Mar 2026 09:55:10 +0100 Subject: [PATCH 33/74] Add SQL Elastic Pool Client and Adapter (#4050) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image --- > [!NOTE] > **Medium Risk** > Introduces a new Azure SQL Elastic Pool discovery adapter and changes SQL Database→Elastic Pool link queries to use a composite `serverName|poolName` key, which could affect any consumers relying on the previous (name-only) lookup behavior. > > **Overview** > Adds first-class discovery for Azure SQL Elastic Pools: a new `SqlElasticPoolClient` wrapper around the Azure SDK plus a `SqlElasticPool` adapter supporting `GET` (by `serverName` + `elasticPoolName`) and `SEARCH` (list by server), including links to the parent SQL Server, related maintenance configuration, and server databases. > > Wires the new adapter into Azure initialization (`adapters.go`), adds a generated GoMock client and comprehensive unit tests, and updates SQL Database linking/tests to resolve elastic pools via the new composite lookup key instead of pool name alone. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b058b540f4f8401a86888beda0e2c8bee2f82f98. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 6794d3f58e2f7ff0de5e4aae608143f1ae8f995e --- .../azure/clients/sql-elastic-pool-client.go | 35 ++ sources/azure/manual/adapters.go | 9 + sources/azure/manual/sql-database.go | 25 +- sources/azure/manual/sql-database_test.go | 4 +- sources/azure/manual/sql-elastic-pool.go | 260 ++++++++++++++ sources/azure/manual/sql-elastic-pool_test.go | 320 ++++++++++++++++++ .../mocks/mock_sql_elastic_pool_client.go | 72 ++++ sources/azure/shared/utils.go | 1 + 8 files changed, 713 insertions(+), 13 deletions(-) create mode 100644 sources/azure/clients/sql-elastic-pool-client.go create mode 100644 sources/azure/manual/sql-elastic-pool.go create mode 100644 sources/azure/manual/sql-elastic-pool_test.go create mode 100644 sources/azure/shared/mocks/mock_sql_elastic_pool_client.go diff --git a/sources/azure/clients/sql-elastic-pool-client.go b/sources/azure/clients/sql-elastic-pool-client.go new file mode 100644 index 00000000..3bb367cd --- /dev/null +++ b/sources/azure/clients/sql-elastic-pool-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" +) + +//go:generate mockgen -destination=../shared/mocks/mock_sql_elastic_pool_client.go -package=mocks -source=sql-elastic-pool-client.go + +// SqlElasticPoolPager is a type alias for the generic Pager interface with SQL elastic pool list response type. +type SqlElasticPoolPager = Pager[armsql.ElasticPoolsClientListByServerResponse] + +// SqlElasticPoolClient is an interface for interacting with Azure SQL elastic pools. +type SqlElasticPoolClient interface { + ListByServer(ctx context.Context, resourceGroupName string, serverName string) SqlElasticPoolPager + Get(ctx context.Context, resourceGroupName string, serverName string, elasticPoolName string) (armsql.ElasticPoolsClientGetResponse, error) +} + +type sqlElasticPoolClient struct { + client *armsql.ElasticPoolsClient +} + +func (a *sqlElasticPoolClient) ListByServer(ctx context.Context, resourceGroupName string, serverName string) SqlElasticPoolPager { + return a.client.NewListByServerPager(resourceGroupName, serverName, nil) +} + +func (a *sqlElasticPoolClient) Get(ctx context.Context, resourceGroupName string, serverName string, elasticPoolName string) (armsql.ElasticPoolsClientGetResponse, error) { + return a.client.Get(ctx, resourceGroupName, serverName, elasticPoolName, nil) +} + +// NewSqlElasticPoolClient creates a new SqlElasticPoolClient from the Azure SDK client. +func NewSqlElasticPoolClient(client *armsql.ElasticPoolsClient) SqlElasticPoolClient { + return &sqlElasticPoolClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index b859cea7..b314e822 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -235,6 +235,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create sql virtual network rules client: %w", err) } + sqlElasticPoolsClient, err := armsql.NewElasticPoolsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create sql elastic pools client: %w", err) + } + postgresqlFlexibleServersClient, err := armpostgresqlflexibleservers.NewServersClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create postgresql flexible servers client: %w", err) @@ -393,6 +398,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewSqlDatabasesClient(sqlDatabasesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewSqlElasticPool( + clients.NewSqlElasticPoolClient(sqlElasticPoolsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewSqlServerFirewallRule( clients.NewSqlServerFirewallRuleClient(sqlFirewallRulesClient), resourceGroupScopes, diff --git a/sources/azure/manual/sql-database.go b/sources/azure/manual/sql-database.go index 1de540a9..b637f0a9 100644 --- a/sources/azure/manual/sql-database.go +++ b/sources/azure/manual/sql-database.go @@ -93,13 +93,13 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, } if database.Properties != nil && database.Properties.ElasticPoolID != nil { - elasticPoolName := azureshared.ExtractSQLElasticPoolNameFromID(*database.Properties.ElasticPoolID) - if elasticPoolName != "" { + elasticPoolServerName, elasticPoolName := azureshared.ExtractSQLElasticPoolInfoFromResourceID(*database.Properties.ElasticPoolID) + if elasticPoolServerName != "" && elasticPoolName != "" { sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ Type: azureshared.SQLElasticPool.String(), Method: sdp.QueryMethod_GET, - Query: elasticPoolName, + Query: shared.CompositeLookupKey(elasticPoolServerName, elasticPoolName), Scope: scope, }, }) @@ -187,15 +187,18 @@ func (s sqlDatabaseWrapper) azureSqlDatabaseToSDPItem(database *armsql.Database, }) case azureshared.SourceResourceTypeSQLElasticPool: + elasticPoolServerName := params["serverName"] elasticPoolName := params["elasticPoolName"] - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: azureshared.SQLElasticPool.String(), - Method: sdp.QueryMethod_GET, - Query: elasticPoolName, - Scope: scope, - }, - }) + if elasticPoolServerName != "" && elasticPoolName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.SQLElasticPool.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(elasticPoolServerName, elasticPoolName), + Scope: scope, + }, + }) + } case azureshared.SourceResourceTypeUnknown: // Synapse SQL Pool and other resource types not yet supported diff --git a/sources/azure/manual/sql-database_test.go b/sources/azure/manual/sql-database_test.go index 11e4eec3..5e976be7 100644 --- a/sources/azure/manual/sql-database_test.go +++ b/sources/azure/manual/sql-database_test.go @@ -161,10 +161,10 @@ func TestSqlDatabase(t *testing.T) { ExpectedQuery: serverName, ExpectedScope: subscriptionID + "." + resourceGroup, }, { - // SQLElasticPool link + // SQLElasticPool link (composite: serverName + elasticPoolName) ExpectedType: azureshared.SQLElasticPool.String(), ExpectedMethod: sdp.QueryMethod_GET, - ExpectedQuery: "test-pool", + ExpectedQuery: shared.CompositeLookupKey("test-server", "test-pool"), ExpectedScope: subscriptionID + "." + resourceGroup, }, { // SQLDatabaseSchema child resource link diff --git a/sources/azure/manual/sql-elastic-pool.go b/sources/azure/manual/sql-elastic-pool.go new file mode 100644 index 00000000..85423f20 --- /dev/null +++ b/sources/azure/manual/sql-elastic-pool.go @@ -0,0 +1,260 @@ +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" + "github.com/overmindtech/cli/sources/shared" +) + +var SQLElasticPoolLookupByName = shared.NewItemTypeLookup("name", azureshared.SQLElasticPool) + +type sqlElasticPoolWrapper struct { + client clients.SqlElasticPoolClient + + *azureshared.MultiResourceGroupBase +} + +func NewSqlElasticPool(client clients.SqlElasticPoolClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &sqlElasticPoolWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_DATABASE, + azureshared.SQLElasticPool, + ), + } +} + +func (s sqlElasticPoolWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: serverName and elasticPoolName", + Scope: scope, + ItemType: s.Type(), + } + } + serverName := queryParts[0] + elasticPoolName := queryParts[1] + if elasticPoolName == "" { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "elasticPoolName cannot be empty", + Scope: scope, + ItemType: s.Type(), + } + } + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + resp, err := s.client.Get(ctx, rgScope.ResourceGroup, serverName, elasticPoolName) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + return s.azureSqlElasticPoolToSDPItem(&resp.ElasticPool, serverName, elasticPoolName, scope) +} + +func (s sqlElasticPoolWrapper) azureSqlElasticPoolToSDPItem(pool *armsql.ElasticPool, serverName, elasticPoolName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(pool, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(serverName, elasticPoolName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.SQLElasticPool.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(pool.Tags), + } + + // Link to parent SQL Server (from resource ID or known server name) + if pool.ID != nil { + extractedServerName := azureshared.ExtractPathParamsFromResourceID(*pool.ID, []string{"servers"}) + if len(extractedServerName) >= 1 && extractedServerName[0] != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.SQLServer.String(), + Method: sdp.QueryMethod_GET, + Query: extractedServerName[0], + Scope: scope, + }, + }) + } + } + if len(sdpItem.GetLinkedItemQueries()) == 0 { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.SQLServer.String(), + Method: sdp.QueryMethod_GET, + Query: serverName, + Scope: scope, + }, + }) + } + + // Link to Maintenance Configuration when set + if pool.Properties != nil && pool.Properties.MaintenanceConfigurationID != nil && *pool.Properties.MaintenanceConfigurationID != "" { + configName := azureshared.ExtractResourceName(*pool.Properties.MaintenanceConfigurationID) + if configName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(*pool.Properties.MaintenanceConfigurationID) + if linkedScope == "" && strings.Contains(*pool.Properties.MaintenanceConfigurationID, "publicMaintenanceConfigurations") { + linkedScope = azureshared.ExtractSubscriptionIDFromResourceID(*pool.Properties.MaintenanceConfigurationID) + } + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.MaintenanceMaintenanceConfiguration.String(), + Method: sdp.QueryMethod_GET, + Query: configName, + Scope: linkedScope, + }, + }) + } + } + + // Link to SQL Databases (child resource; list by server returns all databases; those in this pool reference this pool via ElasticPoolID) + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.SQLDatabase.String(), + Method: sdp.QueryMethod_SEARCH, + Query: serverName, + Scope: scope, + }, + }) + + return sdpItem, nil +} + +func (s sqlElasticPoolWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + SQLServerLookupByName, + SQLElasticPoolLookupByName, + } +} + +func (s sqlElasticPoolWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: serverName", + Scope: scope, + ItemType: s.Type(), + } + } + serverName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + pager := s.client.ListByServer(ctx, rgScope.ResourceGroup, serverName) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + for _, pool := range page.Value { + if pool.Name == nil { + continue + } + item, sdpErr := s.azureSqlElasticPoolToSDPItem(pool, serverName, *pool.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (s sqlElasticPoolWrapper) 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 _, pool := range page.Value { + if pool.Name == nil { + continue + } + item, sdpErr := s.azureSqlElasticPoolToSDPItem(pool, serverName, *pool.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (s sqlElasticPoolWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + SQLServerLookupByName, + }, + } +} + +func (s sqlElasticPoolWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.SQLServer: true, + azureshared.SQLDatabase: true, + azureshared.MaintenanceMaintenanceConfiguration: true, + } +} + +func (s sqlElasticPoolWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_mssql_elasticpool.id", + }, + } +} + +func (s sqlElasticPoolWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Sql/servers/elasticPools/read", + } +} + +func (s sqlElasticPoolWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/sql-elastic-pool_test.go b/sources/azure/manual/sql-elastic-pool_test.go new file mode 100644 index 00000000..189eabca --- /dev/null +++ b/sources/azure/manual/sql-elastic-pool_test.go @@ -0,0 +1,320 @@ +package manual_test + +import ( + "context" + "errors" + "slices" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" + "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" +) + +type mockSqlElasticPoolPager struct { + pages []armsql.ElasticPoolsClientListByServerResponse + index int +} + +func (m *mockSqlElasticPoolPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockSqlElasticPoolPager) NextPage(ctx context.Context) (armsql.ElasticPoolsClientListByServerResponse, error) { + if m.index >= len(m.pages) { + return armsql.ElasticPoolsClientListByServerResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type errorSqlElasticPoolPager struct{} + +func (e *errorSqlElasticPoolPager) More() bool { + return true +} + +func (e *errorSqlElasticPoolPager) NextPage(ctx context.Context) (armsql.ElasticPoolsClientListByServerResponse, error) { + return armsql.ElasticPoolsClientListByServerResponse{}, errors.New("pager error") +} + +type testSqlElasticPoolClient struct { + *mocks.MockSqlElasticPoolClient + pager clients.SqlElasticPoolPager +} + +func (t *testSqlElasticPoolClient) ListByServer(ctx context.Context, resourceGroupName, serverName string) clients.SqlElasticPoolPager { + return t.pager +} + +func TestSqlElasticPool(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + serverName := "test-server" + elasticPoolName := "test-pool" + + t.Run("Get", func(t *testing.T) { + pool := createAzureSqlElasticPool(serverName, elasticPoolName) + + mockClient := mocks.NewMockSqlElasticPoolClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, elasticPoolName).Return( + armsql.ElasticPoolsClientGetResponse{ + ElasticPool: *pool, + }, nil) + + wrapper := manual.NewSqlElasticPool(&testSqlElasticPoolClient{MockSqlElasticPoolClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, elasticPoolName) + 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.SQLElasticPool.String() { + t.Errorf("Expected type %s, got %s", azureshared.SQLElasticPool.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUniqueAttrValue := shared.CompositeLookupKey(serverName, elasticPoolName) + if sdpItem.UniqueAttributeValue() != expectedUniqueAttrValue { + t.Errorf("Expected unique attribute value %s, got %s", expectedUniqueAttrValue, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.SQLServer.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: serverName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, + { + ExpectedType: azureshared.SQLDatabase.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: serverName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSqlElasticPoolClient(ctrl) + wrapper := manual.NewSqlElasticPool(&testSqlElasticPoolClient{MockSqlElasticPoolClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], serverName, true) + if qErr == nil { + t.Error("Expected error when providing only serverName (1 query part), but got nil") + } + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockSqlElasticPoolClient(ctrl) + wrapper := manual.NewSqlElasticPool(&testSqlElasticPoolClient{MockSqlElasticPoolClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, "") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when elastic pool name is empty, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + pool1 := createAzureSqlElasticPool(serverName, "pool-1") + pool2 := createAzureSqlElasticPool(serverName, "pool-2") + + mockClient := mocks.NewMockSqlElasticPoolClient(ctrl) + pager := &mockSqlElasticPoolPager{ + pages: []armsql.ElasticPoolsClientListByServerResponse{ + { + ElasticPoolListResult: armsql.ElasticPoolListResult{ + Value: []*armsql.ElasticPool{pool1, pool2}, + }, + }, + }, + } + + testClient := &testSqlElasticPoolClient{ + MockSqlElasticPoolClient: mockClient, + pager: pager, + } + + wrapper := manual.NewSqlElasticPool(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") + } + + items, qErr := searchable.Search(ctx, wrapper.Scopes()[0], serverName, true) + if qErr != nil { + t.Fatalf("Expected no error from Search, got: %v", qErr) + } + if len(items) != 2 { + t.Errorf("Expected 2 items from Search, got %d", len(items)) + } + }) + + t.Run("SearchStream", func(t *testing.T) { + pool := createAzureSqlElasticPool(serverName, elasticPoolName) + + mockClient := mocks.NewMockSqlElasticPoolClient(ctrl) + pager := &mockSqlElasticPoolPager{ + pages: []armsql.ElasticPoolsClientListByServerResponse{ + { + ElasticPoolListResult: armsql.ElasticPoolListResult{ + Value: []*armsql.ElasticPool{pool}, + }, + }, + }, + } + + testClient := &testSqlElasticPoolClient{ + MockSqlElasticPoolClient: mockClient, + pager: pager, + } + wrapper := manual.NewSqlElasticPool(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchStreamable, ok := adapter.(discovery.SearchStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support SearchStream operation") + } + + stream := discovery.NewRecordingQueryResultStream() + searchStreamable.SearchStream(ctx, wrapper.Scopes()[0], serverName, true, stream) + items := stream.GetItems() + errs := stream.GetErrors() + if len(errs) > 0 { + t.Fatalf("Expected no errors from SearchStream, got: %v", errs) + } + if len(items) != 1 { + t.Errorf("Expected 1 item from SearchStream, got %d", len(items)) + } + }) + + t.Run("SearchWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSqlElasticPoolClient(ctrl) + wrapper := manual.NewSqlElasticPool(&testSqlElasticPoolClient{MockSqlElasticPoolClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("elastic pool not found") + + mockClient := mocks.NewMockSqlElasticPoolClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, "nonexistent-pool").Return( + armsql.ElasticPoolsClientGetResponse{}, expectedErr) + + wrapper := manual.NewSqlElasticPool(&testSqlElasticPoolClient{MockSqlElasticPoolClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, "nonexistent-pool") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent elastic pool, but got nil") + } + }) + + t.Run("ErrorHandling_Search", func(t *testing.T) { + mockClient := mocks.NewMockSqlElasticPoolClient(ctrl) + errorPager := &errorSqlElasticPoolPager{} + testClient := &testSqlElasticPoolClient{ + MockSqlElasticPoolClient: mockClient, + pager: errorPager, + } + + wrapper := manual.NewSqlElasticPool(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0], serverName) + if qErr == nil { + t.Error("Expected error from Search when pager returns error, but got nil") + } + }) + + t.Run("InterfaceCompliance", func(t *testing.T) { + mockClient := mocks.NewMockSqlElasticPoolClient(ctrl) + wrapper := manual.NewSqlElasticPool(&testSqlElasticPoolClient{MockSqlElasticPoolClient: mockClient}, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + w := wrapper.(sources.Wrapper) + + permissions := w.IAMPermissions() + if len(permissions) == 0 { + t.Error("Expected IAMPermissions to return at least one permission") + } + expectedPermission := "Microsoft.Sql/servers/elasticPools/read" + if !slices.Contains(permissions, expectedPermission) { + t.Errorf("Expected IAMPermissions to include %s", expectedPermission) + } + + potentialLinks := w.PotentialLinks() + if !potentialLinks[azureshared.SQLServer] { + t.Error("Expected PotentialLinks to include SQLServer") + } + if !potentialLinks[azureshared.SQLDatabase] { + t.Error("Expected PotentialLinks to include SQLDatabase") + } + if !potentialLinks[azureshared.MaintenanceMaintenanceConfiguration] { + t.Error("Expected PotentialLinks to include MaintenanceMaintenanceConfiguration") + } + + mappings := w.TerraformMappings() + if len(mappings) == 0 { + t.Error("Expected TerraformMappings to return at least one mapping") + } + foundMapping := false + for _, mapping := range mappings { + if mapping.GetTerraformQueryMap() == "azurerm_mssql_elasticpool.id" { + foundMapping = true + break + } + } + if !foundMapping { + t.Error("Expected TerraformMappings to include 'azurerm_mssql_elasticpool.id' mapping") + } + }) +} + +func createAzureSqlElasticPool(serverName, elasticPoolName string) *armsql.ElasticPool { + poolID := "/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Sql/servers/" + serverName + "/elasticPools/" + elasticPoolName + state := armsql.ElasticPoolStateReady + return &armsql.ElasticPool{ + Name: &elasticPoolName, + ID: &poolID, + Properties: &armsql.ElasticPoolProperties{ + State: &state, + }, + } +} diff --git a/sources/azure/shared/mocks/mock_sql_elastic_pool_client.go b/sources/azure/shared/mocks/mock_sql_elastic_pool_client.go new file mode 100644 index 00000000..cf21d653 --- /dev/null +++ b/sources/azure/shared/mocks/mock_sql_elastic_pool_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: sql-elastic-pool-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_sql_elastic_pool_client.go -package=mocks -source=sql-elastic-pool-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armsql "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockSqlElasticPoolClient is a mock of SqlElasticPoolClient interface. +type MockSqlElasticPoolClient struct { + ctrl *gomock.Controller + recorder *MockSqlElasticPoolClientMockRecorder + isgomock struct{} +} + +// MockSqlElasticPoolClientMockRecorder is the mock recorder for MockSqlElasticPoolClient. +type MockSqlElasticPoolClientMockRecorder struct { + mock *MockSqlElasticPoolClient +} + +// NewMockSqlElasticPoolClient creates a new mock instance. +func NewMockSqlElasticPoolClient(ctrl *gomock.Controller) *MockSqlElasticPoolClient { + mock := &MockSqlElasticPoolClient{ctrl: ctrl} + mock.recorder = &MockSqlElasticPoolClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSqlElasticPoolClient) EXPECT() *MockSqlElasticPoolClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockSqlElasticPoolClient) Get(ctx context.Context, resourceGroupName, serverName, elasticPoolName string) (armsql.ElasticPoolsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, serverName, elasticPoolName) + ret0, _ := ret[0].(armsql.ElasticPoolsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockSqlElasticPoolClientMockRecorder) Get(ctx, resourceGroupName, serverName, elasticPoolName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSqlElasticPoolClient)(nil).Get), ctx, resourceGroupName, serverName, elasticPoolName) +} + +// ListByServer mocks base method. +func (m *MockSqlElasticPoolClient) ListByServer(ctx context.Context, resourceGroupName, serverName string) clients.SqlElasticPoolPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByServer", ctx, resourceGroupName, serverName) + ret0, _ := ret[0].(clients.SqlElasticPoolPager) + return ret0 +} + +// ListByServer indicates an expected call of ListByServer. +func (mr *MockSqlElasticPoolClientMockRecorder) ListByServer(ctx, resourceGroupName, serverName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByServer", reflect.TypeOf((*MockSqlElasticPoolClient)(nil).ListByServer), ctx, resourceGroupName, serverName) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 09a64ed1..bbce810b 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -25,6 +25,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-storage-storage-account-private-endpoint-connection": {"storageAccounts", "privateEndpointConnections"}, "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-sql-elastic-pool": {"servers", "elasticPools"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/elasticPools/{elasticPoolName}", "azure-sql-server-firewall-rule": {"servers", "firewallRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/firewallRules/{ruleName}", "azure-sql-server-virtual-network-rule": {"servers", "virtualNetworkRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/virtualNetworkRules/{ruleName}", "azure-dbforpostgresql-database": {"flexibleServers", "databases"}, // "/subscriptions/.../Microsoft.DBforPostgreSQL/flexibleServers/{server}/databases/{db}", From a13c2c150aa683f519be1bb211e7102eb201f5d3 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Mon, 2 Mar 2026 11:47:48 +0100 Subject: [PATCH 34/74] chore(deps): lock file maintenance (#4055) 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](../issues/370) 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: a8c5570455e95dac54d374f4b10d669a83dde1c7 --- .terraform.lock.hcl | 58 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index c59993e2..d517b05a 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.33.0" + version = "6.34.0" constraints = ">= 4.56.0" hashes = [ - "h1:5WOFa3rTDasweoSDN+YUVI4X0sjP8Z/gPCp1yWBzESw=", - "h1:DAUdRFTP+CSl97kUK1ixUEfo4gzFeqDSpqas82B+/JA=", - "h1:MxndtTQD9qHGQwIjScLlY4BQZgdjJ1Lsq1TdwodsxmU=", - "h1:OHXZ+2JAjhcjQAItUsd1beQkdNoTTwvQN9YZG894IsU=", - "h1:c54PuIr7mP2SYdPG2yayENMUGgSJlOmHKOFZAXKjYiM=", - "h1:eHK7wxG22vsMlKQwaPCUCGCu6H7rFF7TgjHY05M+8DE=", - "h1:eho379mi4YF9Lue1Xpq+vpvZzV+kqP9l4QssnDNJgkA=", - "h1:gWcYhi/dga/1U5HRxc2p9fgODsenO7qQ2lXaRwOHfIc=", - "h1:h+u+IPB/QmydJZHUepbvkCWSR2LSZ9rl6xXURbQdbXQ=", - "h1:hrMUZRM4IVU61ZwpLW6CMVmhlQSOajv8L5hSZmVNvQY=", - "h1:iy/Wyzeat5RuwUwENTfWHeXG/sXnTianugicM64kYj0=", - "h1:vwOi4EvCXxCaYLtCy2vaNkKGHNPvz1Oz2ySn3eRtQ50=", - "h1:wNrviem6bg9fq1bYvtGqH9QWO6iWbM1bBRLSqFJWqWM=", - "h1:zBnDfDt2pekMN9B43NJdiDerHvZENG6Wh6okZWusuOs=", - "zh:207f3f9db05c11429a241b84deeecfbd4caa941792c2c49b09c8c85cd59474dd", - "zh:25c36ad1f4617aeb23f8cd18efc7856127db721f6cf3e2e474236af019ce9ad1", - "zh:2685af1f3eb9abfce3168777463eaaad9dba5687f9f84d8bb579cb878bcfa18b", - "zh:57e28457952cf43923533af0a9bb322164be5fc3d66c080b5c59ee81950e9ef6", - "zh:5b6cd074f9e3a8d91841e739d259fe11f181e69c4019e3321231b35c0dde08c8", - "zh:6e3251500cebf1effb9c68d49041268ea270f75b122b94d261af231a8ebfa981", - "zh:7eee56f52f4b94637793508f3e83f68855f5f884a77aed2bd2fe77480c89e33d", + "h1:6yGUU6VTNf/7lBfT+TMY5L8W2crrEGAd45y7/mNzkAM=", + "h1:7T2XHD73DzPlDSa0n7A6zZepkMRNn/N/U6E1DnqElgk=", + "h1:BEEUCSQqnbk/CyUAd/+4bcdtCpihkuoAfFDKCRMLUdc=", + "h1:HMD2NsuZiYGq09bi3vuBGY+evwKBDJHWIK4sWCEdnJU=", + "h1:Ku0EN0HA/3RU8T7M/NCMboOZHyRTi3ULdP/6hnENHSc=", + "h1:OWukIDuti3ZzrjDlrlvEGu8mHRM2VjS17XPWPxZxVHc=", + "h1:Qzr5C24XLiHmkJVuao/Kb+jFLPaxGE/D5GUgko5VdWg=", + "h1:ZGSMOPC+Du0cKJ2kV1Ni8Rnz7ezsIu+jYFEknpye5CM=", + "h1:aG6yVfT0GtYeKbB1rcqMkI/8MCykAkwjWnPzJtisGLA=", + "h1:gZ2YluA7pZ3OB1kIqhhKEsRFHxAF67+lqDm8aCtzr3g=", + "h1:rr+aNXi0UZ5Iwhg7dq0wGflsqTfms6h0cLFBArE+Y+k=", + "h1:tG2N7S54admlDbTjy5/T8QzGivR0WUcBOEahlZRnxUw=", + "h1:wXPejniDcbqRtL2zzaeZsmjLe7NekeYD5QjlIzUOylI=", + "h1:xqm4+M0cYrQRR2VMMU++W2Busqsrg5/VX1f6BnG35Cw=", + "zh:1e49dc96bf50633583e3cbe23bb357642e7e9afe135f54e061e26af6310e50d2", + "zh:45651bb4dad681f17782d99d9324de182a7bb9fbe9dd22f120fdb7fe42969cc9", + "zh:5880c306a427128124585b460c53bbcab9fb3767f26f796eae204f65f111a927", + "zh:71fa9170989b3a1a6913c369bd4a792f4a3e2aab4024c2aff0911e704020b058", + "zh:8d48628fb30f11b04215e06f4dd8a3b32f5f9ea2ed116d0c81c686bf678f9185", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:9e228c92db1b9e36a0f899d6ab7446e6b8cf3183112d4f1c1613d6827a0ed3d6", - "zh:b34a84475e91715352ed1119b21e51a81d8ad12e93c86d4e78cd2d315d02dcab", - "zh:cdcc05a423a78a9b2c4e2844c58ecbf2ce6a3117cab353fa05197782d6f76667", - "zh:d0f5f6b1399cfa1b64f3e824bee9e39ff15d5a540ff197e9bfc157fe354a8426", - "zh:d9525dbb53468dee6b8e6d15669d25957e9872bf1cd386231dff93c8c659f1d7", - "zh:ed37db2df08b961a7fc390164273e602767ca6922f57560daa9678a2e1315fd0", - "zh:f6adc66b86e12041a2d3739600e6a153a1f5752dd363db11469f6f4dbd090080", + "zh:a6885766588fcad887bdac8c3665e048480eda028e492759a1ea29d22b98d509", + "zh:a6ce9f5e7edc2258733e978bba147600b42a979e18575ce2c7d7dcb6d0b9911f", + "zh:c88d8b7d344e745b191509c29ca773d696da8ca3443f62b20f97982d2d33ea00", + "zh:cae90d6641728ad0219b6a84746bf86dd1dda3e31560d6495a202213ef0258b6", + "zh:cc35927d9d41878049c4221beb1d580a3dbadaca7ba39fb267e001ef9c59ccb3", + "zh:d9e1cb00dc33998e1242fb844e4e3e6cf95e57c664dc1eb55bb7d24f8324bad3", + "zh:f3dbf4a1b7020722145312eb4425f3ea356276d741e3f60fb703fc59a1e2d9fd", + "zh:faba832cc9d99a83f42aaf5a27a4c7309401200169ef04643104cfc8f522d007", + "zh:fcd3f30b91dbcc7db67d5d39268741ffa46696a230a1f2aef32d245ace54bf65", ] } From cbbb8c67cad579d5cf58ab62226f6c9c05d0b6f4 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Mon, 2 Mar 2026 11:55:55 +0000 Subject: [PATCH 35/74] Eng 2708 create documentdbprivateendpointconnection adapter (#4060) image > [!NOTE] > **Medium Risk** > Introduces a new Azure discovery adapter and updates Cosmos DB discovery behavior, plus upgrades the `armcosmos` SDK to `v3`, which could affect runtime discovery and paging behavior. Changes are scoped to Azure Cosmos DB resources but touch shared resource-ID parsing mappings and adapter registration. > > **Overview** > Adds discovery support for Cosmos DB private endpoint connections via a new `SearchableWrapper` adapter (with client interface, generated mock, and unit tests) and registers it in `manual/adapters.go`. > > Upgrades the Azure Cosmos SDK dependency to `github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3` and updates existing Cosmos DB database account code/tests to use the new import. > > Enhances the Cosmos DB database account adapter to emit additional *global* links for `DocumentEndpoint`/regional endpoints (to `stdlib.NetworkHTTP`/`stdlib.NetworkDNS`) and for `IPRules` (to `stdlib.NetworkIP`), and extends `GetResourceIDPathKeys` to support parsing IDs for the new private-endpoint-connection resource type. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 12fd2adf04576b4c750b07aea54102704b97cd00. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 8f876ac231cfb68d6e30a760fb71a1373c6c3968 --- go.mod | 2 +- go.sum | 4 +- .../documentdb-database-accounts-client.go | 2 +- ...ntdb-private-endpoint-connection-client.go | 35 ++ .../documentdb-database-accounts_test.go | 2 +- sources/azure/manual/adapters.go | 12 +- .../manual/documentdb-database-accounts.go | 49 ++- .../documentdb-database-accounts_test.go | 2 +- .../documentdb-private-endpoint-connection.go | 236 +++++++++++++ ...mentdb-private-endpoint-connection_test.go | 320 ++++++++++++++++++ ...ock_documentdb_database_accounts_client.go | 2 +- ...ntdb_private_endpoint_connection_client.go | 72 ++++ sources/azure/shared/utils.go | 1 + 13 files changed, 730 insertions(+), 9 deletions(-) create mode 100644 sources/azure/clients/documentdb-private-endpoint-connection-client.go create mode 100644 sources/azure/manual/documentdb-private-endpoint-connection.go create mode 100644 sources/azure/manual/documentdb-private-endpoint-connection_test.go create mode 100644 sources/azure/shared/mocks/mock_documentdb_private_endpoint_connection_client.go diff --git a/go.mod b/go.mod index 77d54301..272bd74e 100644 --- a/go.mod +++ b/go.mod @@ -46,7 +46,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3 v3.0.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.3.0 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos v1.0.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3 v3.4.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2 v2.0.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 diff --git a/go.sum b/go.sum index a1084bbf..0b47b545 100644 --- a/go.sum +++ b/go.sum @@ -118,8 +118,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3 v3.0.1 h github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3 v3.0.1/go.mod h1:kz6cfDXtcUJWUjLKSlXW+oBqtWovK648UYJDZYtAZ3g= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.3.0 h1:nyxugFxG2uhbMeJVCFFuD2j9wu+6KgeabITdINraQsE= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.3.0/go.mod h1:e4RAYykLIz73CF52KhSooo4whZGXvXrD09m0jkgnWiU= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos v1.0.0 h1:Fv8iibGn1eSw0lt2V3cTsuokBEnOP+M//n8OiMcCgTM= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos v1.0.0/go.mod h1:Qpe/qN9d5IQ7WPtTXMRCd6+BWTnhi3sxXVys6oJ5Vho= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3 v3.4.0 h1:+EhRnIOLvffCvUMUfP+MgOp6PrtN1d6xt94DZtrC3lA= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3 v3.4.0/go.mod h1:Bb7kqorvA2acMCNFac+2ldoQWi7QrcMdH+9Gg9C7fSM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= diff --git a/sources/azure/clients/documentdb-database-accounts-client.go b/sources/azure/clients/documentdb-database-accounts-client.go index 2c6ce075..6fd66c2e 100644 --- a/sources/azure/clients/documentdb-database-accounts-client.go +++ b/sources/azure/clients/documentdb-database-accounts-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3" ) //go:generate mockgen -destination=../shared/mocks/mock_documentdb_database_accounts_client.go -package=mocks -source=documentdb-database-accounts-client.go diff --git a/sources/azure/clients/documentdb-private-endpoint-connection-client.go b/sources/azure/clients/documentdb-private-endpoint-connection-client.go new file mode 100644 index 00000000..6fe021f0 --- /dev/null +++ b/sources/azure/clients/documentdb-private-endpoint-connection-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3" +) + +//go:generate mockgen -destination=../shared/mocks/mock_documentdb_private_endpoint_connection_client.go -package=mocks -source=documentdb-private-endpoint-connection-client.go + +// DocumentDBPrivateEndpointConnectionsPager is a type alias for the generic Pager interface with Cosmos DB private endpoint connection list response type. +type DocumentDBPrivateEndpointConnectionsPager = Pager[armcosmos.PrivateEndpointConnectionsClientListByDatabaseAccountResponse] + +// DocumentDBPrivateEndpointConnectionsClient is an interface for interacting with Azure Cosmos DB (DocumentDB) database account private endpoint connections. +type DocumentDBPrivateEndpointConnectionsClient interface { + Get(ctx context.Context, resourceGroupName string, accountName string, privateEndpointConnectionName string) (armcosmos.PrivateEndpointConnectionsClientGetResponse, error) + ListByDatabaseAccount(ctx context.Context, resourceGroupName string, accountName string) DocumentDBPrivateEndpointConnectionsPager +} + +type documentDBPrivateEndpointConnectionsClient struct { + client *armcosmos.PrivateEndpointConnectionsClient +} + +func (c *documentDBPrivateEndpointConnectionsClient) Get(ctx context.Context, resourceGroupName string, accountName string, privateEndpointConnectionName string) (armcosmos.PrivateEndpointConnectionsClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, accountName, privateEndpointConnectionName, nil) +} + +func (c *documentDBPrivateEndpointConnectionsClient) ListByDatabaseAccount(ctx context.Context, resourceGroupName string, accountName string) DocumentDBPrivateEndpointConnectionsPager { + return c.client.NewListByDatabaseAccountPager(resourceGroupName, accountName, nil) +} + +// NewDocumentDBPrivateEndpointConnectionsClient creates a new DocumentDBPrivateEndpointConnectionsClient from the Azure SDK client. +func NewDocumentDBPrivateEndpointConnectionsClient(client *armcosmos.PrivateEndpointConnectionsClient) DocumentDBPrivateEndpointConnectionsClient { + return &documentDBPrivateEndpointConnectionsClient{client: client} +} diff --git a/sources/azure/integration-tests/documentdb-database-accounts_test.go b/sources/azure/integration-tests/documentdb-database-accounts_test.go index 69ea4ce8..8b8eb3e3 100644 --- a/sources/azure/integration-tests/documentdb-database-accounts_test.go +++ b/sources/azure/integration-tests/documentdb-database-accounts_test.go @@ -10,7 +10,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" log "github.com/sirupsen/logrus" diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index b314e822..81c34f77 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -8,7 +8,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" @@ -146,6 +146,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create document db database accounts client: %w", err) } + documentDBPrivateEndpointConnectionsClient, err := armcosmos.NewPrivateEndpointConnectionsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create document db private endpoint connections client: %w", err) + } + keyVaultsClient, err := armkeyvault.NewVaultsClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create key vaults client: %w", err) @@ -414,6 +419,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewDocumentDBDatabaseAccountsClient(documentDBDatabaseAccountsClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewDocumentDBPrivateEndpointConnection( + clients.NewDocumentDBPrivateEndpointConnectionsClient(documentDBPrivateEndpointConnectionsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewKeyVaultVault( clients.NewVaultsClient(keyVaultsClient), resourceGroupScopes, @@ -597,6 +606,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewSqlServerFirewallRule(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewSqlServerVirtualNetworkRule(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDocumentDBDatabaseAccounts(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewDocumentDBPrivateEndpointConnection(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewKeyVaultVault(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewKeyVaultManagedHSM(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDBforPostgreSQLDatabase(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/documentdb-database-accounts.go b/sources/azure/manual/documentdb-database-accounts.go index 28b12c20..7901faa7 100644 --- a/sources/azure/manual/documentdb-database-accounts.go +++ b/sources/azure/manual/documentdb-database-accounts.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/go/sdpcache" @@ -13,6 +13,7 @@ import ( "github.com/overmindtech/cli/sources/azure/clients" azureshared "github.com/overmindtech/cli/sources/azure/shared" "github.com/overmindtech/cli/sources/shared" + "github.com/overmindtech/cli/sources/stdlib" ) var DocumentDBDatabaseAccountsLookupByName = shared.NewItemTypeLookup("name", azureshared.DocumentDBDatabaseAccounts) @@ -289,6 +290,49 @@ func (s documentDBDatabaseAccountsWrapper) azureDocumentDBDatabaseAccountToSDPIt } } + // Link to stdlib for document endpoint and regional endpoints (DNS/HTTP) + linkedDNSHostnames := make(map[string]struct{}) + seenIPs := make(map[string]struct{}) + if account.Properties != nil && account.Properties.DocumentEndpoint != nil && *account.Properties.DocumentEndpoint != "" { + AppendURILinks(&sdpItem.LinkedItemQueries, *account.Properties.DocumentEndpoint, linkedDNSHostnames, seenIPs) + } + if account.Properties != nil { + for _, loc := range account.Properties.ReadLocations { + if loc != nil && loc.DocumentEndpoint != nil && *loc.DocumentEndpoint != "" { + AppendURILinks(&sdpItem.LinkedItemQueries, *loc.DocumentEndpoint, linkedDNSHostnames, seenIPs) + } + } + for _, loc := range account.Properties.WriteLocations { + if loc != nil && loc.DocumentEndpoint != nil && *loc.DocumentEndpoint != "" { + AppendURILinks(&sdpItem.LinkedItemQueries, *loc.DocumentEndpoint, linkedDNSHostnames, seenIPs) + } + } + for _, loc := range account.Properties.Locations { + if loc != nil && loc.DocumentEndpoint != nil && *loc.DocumentEndpoint != "" { + AppendURILinks(&sdpItem.LinkedItemQueries, *loc.DocumentEndpoint, linkedDNSHostnames, seenIPs) + } + } + // Link to stdlib.NetworkIP for IP rules (single IPv4 or CIDR) + if account.Properties.IPRules != nil { + for _, rule := range account.Properties.IPRules { + if rule != nil && rule.IPAddressOrRange != nil && *rule.IPAddressOrRange != "" { + val := *rule.IPAddressOrRange + if _, seen := seenIPs[val]; !seen { + seenIPs[val] = struct{}{} + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: val, + Scope: "global", + }, + }) + } + } + } + } + } + return sdpItem, nil } @@ -305,6 +349,9 @@ func (s documentDBDatabaseAccountsWrapper) PotentialLinks() map[shared.ItemType] azureshared.NetworkSubnet, azureshared.KeyVaultVault, azureshared.ManagedIdentityUserAssignedIdentity, + stdlib.NetworkIP, + stdlib.NetworkDNS, + stdlib.NetworkHTTP, ) } diff --git a/sources/azure/manual/documentdb-database-accounts_test.go b/sources/azure/manual/documentdb-database-accounts_test.go index d59160b7..2c6a3e51 100644 --- a/sources/azure/manual/documentdb-database-accounts_test.go +++ b/sources/azure/manual/documentdb-database-accounts_test.go @@ -5,7 +5,7 @@ import ( "errors" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/go/discovery" diff --git a/sources/azure/manual/documentdb-private-endpoint-connection.go b/sources/azure/manual/documentdb-private-endpoint-connection.go new file mode 100644 index 00000000..7030ceb7 --- /dev/null +++ b/sources/azure/manual/documentdb-private-endpoint-connection.go @@ -0,0 +1,236 @@ +package manual + +import ( + "context" + "errors" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/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" +) + +var DocumentDBPrivateEndpointConnectionLookupByName = shared.NewItemTypeLookup("name", azureshared.DocumentDBPrivateEndpointConnection) + +type documentDBPrivateEndpointConnectionWrapper struct { + client clients.DocumentDBPrivateEndpointConnectionsClient + + *azureshared.MultiResourceGroupBase +} + +// NewDocumentDBPrivateEndpointConnection returns a SearchableWrapper for Azure Cosmos DB (DocumentDB) database account private endpoint connections. +func NewDocumentDBPrivateEndpointConnection(client clients.DocumentDBPrivateEndpointConnectionsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &documentDBPrivateEndpointConnectionWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_DATABASE, + azureshared.DocumentDBPrivateEndpointConnection, + ), + } +} + +func (s documentDBPrivateEndpointConnectionWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: accountName and privateEndpointConnectionName", + Scope: scope, + ItemType: s.Type(), + } + } + accountName := queryParts[0] + connectionName := queryParts[1] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + resp, err := s.client.Get(ctx, rgScope.ResourceGroup, accountName, connectionName) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + item, sdpErr := s.azurePrivateEndpointConnectionToSDPItem(&resp.PrivateEndpointConnection, accountName, connectionName, scope) + if sdpErr != nil { + return nil, sdpErr + } + return item, nil +} + +func (s documentDBPrivateEndpointConnectionWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + DocumentDBDatabaseAccountsLookupByName, + DocumentDBPrivateEndpointConnectionLookupByName, + } +} + +func (s documentDBPrivateEndpointConnectionWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: accountName", + Scope: scope, + ItemType: s.Type(), + } + } + accountName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + pager := s.client.ListByDatabaseAccount(ctx, rgScope.ResourceGroup, accountName) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + for _, conn := range page.Value { + if conn.Name == nil { + continue + } + + item, sdpErr := s.azurePrivateEndpointConnectionToSDPItem(conn, accountName, *conn.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (s documentDBPrivateEndpointConnectionWrapper) 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: accountName"), scope, s.Type())) + return + } + accountName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.ListByDatabaseAccount(ctx, rgScope.ResourceGroup, accountName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, conn := range page.Value { + if conn.Name == nil { + continue + } + item, sdpErr := s.azurePrivateEndpointConnectionToSDPItem(conn, accountName, *conn.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (s documentDBPrivateEndpointConnectionWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + DocumentDBDatabaseAccountsLookupByName, + }, + } +} + +func (s documentDBPrivateEndpointConnectionWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.DocumentDBDatabaseAccounts: true, + azureshared.NetworkPrivateEndpoint: true, + } +} + +func (s documentDBPrivateEndpointConnectionWrapper) azurePrivateEndpointConnectionToSDPItem(conn *armcosmos.PrivateEndpointConnection, accountName, connectionName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(conn) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(accountName, connectionName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.DocumentDBPrivateEndpointConnection.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + } + + // Health from provisioning state (Cosmos uses *string, not an enum) + if conn.Properties != nil && conn.Properties.ProvisioningState != nil { + state := strings.ToLower(*conn.Properties.ProvisioningState) + switch state { + case "succeeded": + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case "creating", "deleting": + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case "failed": + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + // Link to parent DocumentDB Database Account + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.DocumentDBDatabaseAccounts.String(), + Method: sdp.QueryMethod_GET, + Query: accountName, + Scope: scope, + }, + }) + + // Link to Network Private Endpoint when present (may be in different resource group) + if conn.Properties != nil && conn.Properties.PrivateEndpoint != nil && conn.Properties.PrivateEndpoint.ID != nil { + peID := *conn.Properties.PrivateEndpoint.ID + peName := azureshared.ExtractResourceName(peID) + if peName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(peID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPrivateEndpoint.String(), + Method: sdp.QueryMethod_GET, + Query: peName, + Scope: linkedScope, + }, + }) + } + } + + return sdpItem, nil +} + +func (s documentDBPrivateEndpointConnectionWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.DocumentDB/databaseAccounts/privateEndpointConnections/read", + } +} + +func (s documentDBPrivateEndpointConnectionWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/documentdb-private-endpoint-connection_test.go b/sources/azure/manual/documentdb-private-endpoint-connection_test.go new file mode 100644 index 00000000..ed53de9f --- /dev/null +++ b/sources/azure/manual/documentdb-private-endpoint-connection_test.go @@ -0,0 +1,320 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3" + "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" +) + +type mockDocumentDBPrivateEndpointConnectionsPager struct { + pages []armcosmos.PrivateEndpointConnectionsClientListByDatabaseAccountResponse + index int +} + +func (m *mockDocumentDBPrivateEndpointConnectionsPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockDocumentDBPrivateEndpointConnectionsPager) NextPage(ctx context.Context) (armcosmos.PrivateEndpointConnectionsClientListByDatabaseAccountResponse, error) { + if m.index >= len(m.pages) { + return armcosmos.PrivateEndpointConnectionsClientListByDatabaseAccountResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type testDocumentDBPrivateEndpointConnectionsClient struct { + *mocks.MockDocumentDBPrivateEndpointConnectionsClient + pager clients.DocumentDBPrivateEndpointConnectionsPager +} + +func (t *testDocumentDBPrivateEndpointConnectionsClient) ListByDatabaseAccount(ctx context.Context, resourceGroupName, accountName string) clients.DocumentDBPrivateEndpointConnectionsPager { + return t.pager +} + +func TestDocumentDBPrivateEndpointConnection(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + accountName := "test-cosmos-account" + connectionName := "test-pec" + + t.Run("Get", func(t *testing.T) { + conn := createAzureDocumentDBPrivateEndpointConnection(connectionName, "") + + mockClient := mocks.NewMockDocumentDBPrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, accountName, connectionName).Return( + armcosmos.PrivateEndpointConnectionsClientGetResponse{ + PrivateEndpointConnection: *conn, + }, nil) + + testClient := &testDocumentDBPrivateEndpointConnectionsClient{MockDocumentDBPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewDocumentDBPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(accountName, connectionName) + 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.DocumentDBPrivateEndpointConnection.String() { + t.Errorf("Expected type %s, got %s", azureshared.DocumentDBPrivateEndpointConnection, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != shared.CompositeLookupKey(accountName, connectionName) { + t.Errorf("Expected unique attribute value %s, got %s", shared.CompositeLookupKey(accountName, connectionName), sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + linkedQueries := sdpItem.GetLinkedItemQueries() + if len(linkedQueries) < 1 { + t.Fatalf("Expected at least 1 linked query, got: %d", len(linkedQueries)) + } + + foundDocumentDBAccount := false + for _, lq := range linkedQueries { + if lq.GetQuery().GetType() == azureshared.DocumentDBDatabaseAccounts.String() { + foundDocumentDBAccount = true + if lq.GetQuery().GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected DocumentDBDatabaseAccounts link method GET, got %v", lq.GetQuery().GetMethod()) + } + if lq.GetQuery().GetQuery() != accountName { + t.Errorf("Expected DocumentDBDatabaseAccounts query %s, got %s", accountName, lq.GetQuery().GetQuery()) + } + } + } + if !foundDocumentDBAccount { + t.Error("Expected linked query to DocumentDBDatabaseAccounts") + } + }) + }) + + t.Run("Get_WithPrivateEndpointLink", func(t *testing.T) { + peID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-pe" + conn := createAzureDocumentDBPrivateEndpointConnection(connectionName, peID) + + mockClient := mocks.NewMockDocumentDBPrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, accountName, connectionName).Return( + armcosmos.PrivateEndpointConnectionsClientGetResponse{ + PrivateEndpointConnection: *conn, + }, nil) + + testClient := &testDocumentDBPrivateEndpointConnectionsClient{MockDocumentDBPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewDocumentDBPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(accountName, connectionName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + foundPrivateEndpoint := false + for _, lq := range sdpItem.GetLinkedItemQueries() { + if lq.GetQuery().GetType() == azureshared.NetworkPrivateEndpoint.String() { + foundPrivateEndpoint = true + if lq.GetQuery().GetQuery() != "test-pe" { + t.Errorf("Expected NetworkPrivateEndpoint query 'test-pe', got %s", lq.GetQuery().GetQuery()) + } + break + } + } + if !foundPrivateEndpoint { + t.Error("Expected linked query to NetworkPrivateEndpoint when PrivateEndpoint ID is set") + } + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockDocumentDBPrivateEndpointConnectionsClient(ctrl) + testClient := &testDocumentDBPrivateEndpointConnectionsClient{MockDocumentDBPrivateEndpointConnectionsClient: mockClient} + + wrapper := manual.NewDocumentDBPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], accountName, true) + if qErr == nil { + t.Error("Expected error when providing insufficient query parts, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + conn1 := createAzureDocumentDBPrivateEndpointConnection("pec-1", "") + conn2 := createAzureDocumentDBPrivateEndpointConnection("pec-2", "") + + mockClient := mocks.NewMockDocumentDBPrivateEndpointConnectionsClient(ctrl) + mockPager := &mockDocumentDBPrivateEndpointConnectionsPager{ + pages: []armcosmos.PrivateEndpointConnectionsClientListByDatabaseAccountResponse{ + { + PrivateEndpointConnectionListResult: armcosmos.PrivateEndpointConnectionListResult{ + Value: []*armcosmos.PrivateEndpointConnection{conn1, conn2}, + }, + }, + }, + } + + testClient := &testDocumentDBPrivateEndpointConnectionsClient{ + MockDocumentDBPrivateEndpointConnectionsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewDocumentDBPrivateEndpointConnection(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, wrapper.Scopes()[0], accountName, 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.Fatalf("Expected no validation error, got: %v", err) + } + if item.GetType() != azureshared.DocumentDBPrivateEndpointConnection.String() { + t.Errorf("Expected type %s, got %s", azureshared.DocumentDBPrivateEndpointConnection, item.GetType()) + } + } + }) + + t.Run("Search_NilNameSkipped", func(t *testing.T) { + validConn := createAzureDocumentDBPrivateEndpointConnection("valid-pec", "") + + mockClient := mocks.NewMockDocumentDBPrivateEndpointConnectionsClient(ctrl) + mockPager := &mockDocumentDBPrivateEndpointConnectionsPager{ + pages: []armcosmos.PrivateEndpointConnectionsClientListByDatabaseAccountResponse{ + { + PrivateEndpointConnectionListResult: armcosmos.PrivateEndpointConnectionListResult{ + Value: []*armcosmos.PrivateEndpointConnection{ + {Name: nil}, + validConn, + }, + }, + }, + }, + } + + testClient := &testDocumentDBPrivateEndpointConnectionsClient{ + MockDocumentDBPrivateEndpointConnectionsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewDocumentDBPrivateEndpointConnection(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, wrapper.Scopes()[0], accountName, 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)) + } + if sdpItems[0].UniqueAttributeValue() != shared.CompositeLookupKey(accountName, "valid-pec") { + t.Errorf("Expected unique value %s, got %s", shared.CompositeLookupKey(accountName, "valid-pec"), sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockDocumentDBPrivateEndpointConnectionsClient(ctrl) + testClient := &testDocumentDBPrivateEndpointConnectionsClient{MockDocumentDBPrivateEndpointConnectionsClient: mockClient} + + wrapper := manual.NewDocumentDBPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("private endpoint connection not found") + + mockClient := mocks.NewMockDocumentDBPrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, accountName, "nonexistent-pec").Return( + armcosmos.PrivateEndpointConnectionsClientGetResponse{}, expectedErr) + + testClient := &testDocumentDBPrivateEndpointConnectionsClient{MockDocumentDBPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewDocumentDBPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(accountName, "nonexistent-pec") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent private endpoint connection, but got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + wrapper := manual.NewDocumentDBPrivateEndpointConnection(nil, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + links := wrapper.PotentialLinks() + if !links[azureshared.DocumentDBDatabaseAccounts] { + t.Error("Expected DocumentDBDatabaseAccounts in PotentialLinks") + } + if !links[azureshared.NetworkPrivateEndpoint] { + t.Error("Expected NetworkPrivateEndpoint in PotentialLinks") + } + }) +} + +func createAzureDocumentDBPrivateEndpointConnection(connectionName, privateEndpointID string) *armcosmos.PrivateEndpointConnection { + conn := &armcosmos.PrivateEndpointConnection{ + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.DocumentDB/databaseAccounts/test-cosmos-account/privateEndpointConnections/" + connectionName), + Name: new(connectionName), + Type: new("Microsoft.DocumentDB/databaseAccounts/privateEndpointConnections"), + Properties: &armcosmos.PrivateEndpointConnectionProperties{ + ProvisioningState: new("Succeeded"), + PrivateLinkServiceConnectionState: &armcosmos.PrivateLinkServiceConnectionStateProperty{ + Status: new("Approved"), + }, + }, + } + if privateEndpointID != "" { + conn.Properties.PrivateEndpoint = &armcosmos.PrivateEndpointProperty{ + ID: new(privateEndpointID), + } + } + return conn +} diff --git a/sources/azure/shared/mocks/mock_documentdb_database_accounts_client.go b/sources/azure/shared/mocks/mock_documentdb_database_accounts_client.go index 59236e73..3da33d1e 100644 --- a/sources/azure/shared/mocks/mock_documentdb_database_accounts_client.go +++ b/sources/azure/shared/mocks/mock_documentdb_database_accounts_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armcosmos "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos" + armcosmos "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_documentdb_private_endpoint_connection_client.go b/sources/azure/shared/mocks/mock_documentdb_private_endpoint_connection_client.go new file mode 100644 index 00000000..dd7d9c69 --- /dev/null +++ b/sources/azure/shared/mocks/mock_documentdb_private_endpoint_connection_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: documentdb-private-endpoint-connection-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_documentdb_private_endpoint_connection_client.go -package=mocks -source=documentdb-private-endpoint-connection-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armcosmos "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockDocumentDBPrivateEndpointConnectionsClient is a mock of DocumentDBPrivateEndpointConnectionsClient interface. +type MockDocumentDBPrivateEndpointConnectionsClient struct { + ctrl *gomock.Controller + recorder *MockDocumentDBPrivateEndpointConnectionsClientMockRecorder + isgomock struct{} +} + +// MockDocumentDBPrivateEndpointConnectionsClientMockRecorder is the mock recorder for MockDocumentDBPrivateEndpointConnectionsClient. +type MockDocumentDBPrivateEndpointConnectionsClientMockRecorder struct { + mock *MockDocumentDBPrivateEndpointConnectionsClient +} + +// NewMockDocumentDBPrivateEndpointConnectionsClient creates a new mock instance. +func NewMockDocumentDBPrivateEndpointConnectionsClient(ctrl *gomock.Controller) *MockDocumentDBPrivateEndpointConnectionsClient { + mock := &MockDocumentDBPrivateEndpointConnectionsClient{ctrl: ctrl} + mock.recorder = &MockDocumentDBPrivateEndpointConnectionsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDocumentDBPrivateEndpointConnectionsClient) EXPECT() *MockDocumentDBPrivateEndpointConnectionsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockDocumentDBPrivateEndpointConnectionsClient) Get(ctx context.Context, resourceGroupName, accountName, privateEndpointConnectionName string) (armcosmos.PrivateEndpointConnectionsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, accountName, privateEndpointConnectionName) + ret0, _ := ret[0].(armcosmos.PrivateEndpointConnectionsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockDocumentDBPrivateEndpointConnectionsClientMockRecorder) Get(ctx, resourceGroupName, accountName, privateEndpointConnectionName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDocumentDBPrivateEndpointConnectionsClient)(nil).Get), ctx, resourceGroupName, accountName, privateEndpointConnectionName) +} + +// ListByDatabaseAccount mocks base method. +func (m *MockDocumentDBPrivateEndpointConnectionsClient) ListByDatabaseAccount(ctx context.Context, resourceGroupName, accountName string) clients.DocumentDBPrivateEndpointConnectionsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByDatabaseAccount", ctx, resourceGroupName, accountName) + ret0, _ := ret[0].(clients.DocumentDBPrivateEndpointConnectionsPager) + return ret0 +} + +// ListByDatabaseAccount indicates an expected call of ListByDatabaseAccount. +func (mr *MockDocumentDBPrivateEndpointConnectionsClientMockRecorder) ListByDatabaseAccount(ctx, resourceGroupName, accountName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByDatabaseAccount", reflect.TypeOf((*MockDocumentDBPrivateEndpointConnectionsClient)(nil).ListByDatabaseAccount), ctx, resourceGroupName, accountName) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index bbce810b..faba83bf 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -23,6 +23,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-storage-encryption-scope": {"storageAccounts", "encryptionScopes"}, "azure-storage-file-share": {"storageAccounts", "shares"}, "azure-storage-storage-account-private-endpoint-connection": {"storageAccounts", "privateEndpointConnections"}, + "azure-documentdb-private-endpoint-connection": {"databaseAccounts", "privateEndpointConnections"}, "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-sql-elastic-pool": {"servers", "elasticPools"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/elasticPools/{elasticPoolName}", From 8e24013e1a209e6098bb759ef44fcd8110e534ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:54:07 +0000 Subject: [PATCH 36/74] fix(deps): update go (#3952) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit > ℹ️ **Note** > > This PR body was truncated due to platform limits. This PR contains the following updates: | Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) | |---|---|---|---| | [buf.build/go/protovalidate](https://redirect.github.com/bufbuild/protovalidate-go) | `v1.1.2` → `v1.1.3` | ![age](https://developer.mend.io/api/mc/badges/age/go/buf.build%2fgo%2fprotovalidate/v1.1.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/buf.build%2fgo%2fprotovalidate/v1.1.2/v1.1.3?slim=true) | | [cloud.google.com/go/aiplatform](https://redirect.github.com/googleapis/google-cloud-go) | `v1.116.0` → `v1.118.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/cloud.google.com%2fgo%2faiplatform/v1.118.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/cloud.google.com%2fgo%2faiplatform/v1.116.0/v1.118.0?slim=true) | | [cloud.google.com/go/auth](https://redirect.github.com/googleapis/google-cloud-go) | `v0.18.1` → `v0.18.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/cloud.google.com%2fgo%2fauth/v0.18.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/cloud.google.com%2fgo%2fauth/v0.18.1/v0.18.2?slim=true) | | [cloud.google.com/go/bigquery](https://redirect.github.com/googleapis/google-cloud-go) | `v1.73.1` → `v1.74.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/cloud.google.com%2fgo%2fbigquery/v1.74.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/cloud.google.com%2fgo%2fbigquery/v1.73.1/v1.74.0?slim=true) | | [cloud.google.com/go/compute](https://redirect.github.com/googleapis/google-cloud-go) | `v1.54.0` → `v1.55.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/cloud.google.com%2fgo%2fcompute/v1.55.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/cloud.google.com%2fgo%2fcompute/v1.54.0/v1.55.0?slim=true) | | [cloud.google.com/go/dataproc/v2](https://redirect.github.com/googleapis/google-cloud-go) | `v2.15.0` → `v2.16.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/cloud.google.com%2fgo%2fdataproc%2fv2/v2.16.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/cloud.google.com%2fgo%2fdataproc%2fv2/v2.15.0/v2.16.0?slim=true) | | [cloud.google.com/go/kms](https://redirect.github.com/googleapis/google-cloud-go) | `v1.25.0` → `v1.26.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/cloud.google.com%2fgo%2fkms/v1.26.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/cloud.google.com%2fgo%2fkms/v1.25.0/v1.26.0?slim=true) | | [github.com/a-h/templ](https://redirect.github.com/a-h/templ) | `v0.3.977` → `v0.3.1001` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fa-h%2ftempl/v0.3.1001?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fa-h%2ftempl/v0.3.977/v0.3.1001?slim=true) | | [github.com/a-h/templ/cmd/templ](https://redirect.github.com/a-h/templ) | `v0.3.977` → `v0.3.1001` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fa-h%2ftempl%2fcmd%2ftempl/v0.3.1001?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fa-h%2ftempl%2fcmd%2ftempl/v0.3.977/v0.3.1001?slim=true) | | [github.com/auth0/go-auth0/v2](https://redirect.github.com/auth0/go-auth0) | `v2.5.0` → `v2.6.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fauth0%2fgo-auth0%2fv2/v2.6.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fauth0%2fgo-auth0%2fv2/v2.5.0/v2.6.0?slim=true) | | [github.com/aws/aws-sdk-go-v2](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.41.1` → `v1.41.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2/v1.41.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2/v1.41.1/v1.41.2?slim=true) | | [github.com/aws/aws-sdk-go-v2/config](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.32.7` → `v1.32.10` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fconfig/v1.32.10?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fconfig/v1.32.7/v1.32.10?slim=true) | | [github.com/aws/aws-sdk-go-v2/credentials](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.19.7` → `v1.19.10` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fcredentials/v1.19.10?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fcredentials/v1.19.7/v1.19.10?slim=true) | | [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.18.17` → `v1.18.18` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2ffeature%2fec2%2fimds/v1.18.18?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2ffeature%2fec2%2fimds/v1.18.17/v1.18.18?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/apigateway](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.38.4` → `v1.38.5` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fapigateway/v1.38.5?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fapigateway/v1.38.4/v1.38.5?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/autoscaling](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.64.0` → `v1.64.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fautoscaling/v1.64.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fautoscaling/v1.64.0/v1.64.1?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/cloudfront](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.60.0` → `v1.60.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fcloudfront/v1.60.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fcloudfront/v1.60.0/v1.60.1?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/cloudwatch](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.53.1` → `v1.55.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fcloudwatch/v1.55.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fcloudwatch/v1.53.1/v1.55.0?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/directconnect](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.38.11` → `v1.38.12` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fdirectconnect/v1.38.12?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fdirectconnect/v1.38.11/v1.38.12?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/dynamodb](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.55.0` → `v1.56.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fdynamodb/v1.56.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fdynamodb/v1.55.0/v1.56.0?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/ec2](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.288.0` → `v1.293.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fec2/v1.293.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fec2/v1.288.0/v1.293.0?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/ecs](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.71.0` → `v1.73.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fecs/v1.73.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fecs/v1.71.0/v1.73.0?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/efs](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.41.10` → `v1.41.11` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fefs/v1.41.11?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fefs/v1.41.10/v1.41.11?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/eks](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.80.0` → `v1.80.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2feks/v1.80.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2feks/v1.80.0/v1.80.1?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.33.19` → `v1.33.20` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2felasticloadbalancing/v1.33.20?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2felasticloadbalancing/v1.33.19/v1.33.20?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.54.6` → `v1.54.7` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2felasticloadbalancingv2/v1.54.7?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2felasticloadbalancingv2/v1.54.6/v1.54.7?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/iam](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.53.2` → `v1.53.3` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fiam/v1.53.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fiam/v1.53.2/v1.53.3?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/kms](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.49.5` → `v1.50.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fkms/v1.50.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fkms/v1.49.5/v1.50.1?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/lambda](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.88.0` → `v1.88.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2flambda/v1.88.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2flambda/v1.88.0/v1.88.1?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/networkfirewall](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.59.3` → `v1.59.4` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fnetworkfirewall/v1.59.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fnetworkfirewall/v1.59.3/v1.59.4?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/networkmanager](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.41.4` → `v1.41.5` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fnetworkmanager/v1.41.5?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fnetworkmanager/v1.41.4/v1.41.5?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/rds](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.115.0` → `v1.116.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2frds/v1.116.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2frds/v1.115.0/v1.116.1?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/route53](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.62.1` → `v1.62.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2froute53/v1.62.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2froute53/v1.62.1/v1.62.2?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/s3](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.96.0` → `v1.96.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fs3/v1.96.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fs3/v1.96.0/v1.96.2?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/sesv2](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.59.1` → `v1.59.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsesv2/v1.59.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsesv2/v1.59.1/v1.59.2?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/sns](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.39.11` → `v1.39.12` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsns/v1.39.12?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsns/v1.39.11/v1.39.12?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/sqs](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.42.21` → `v1.42.22` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsqs/v1.42.22?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsqs/v1.42.21/v1.42.22?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/ssm](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.67.8` → `v1.68.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fssm/v1.68.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fssm/v1.67.8/v1.68.1?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/sts](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.41.6` → `v1.41.7` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsts/v1.41.7?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsts/v1.41.6/v1.41.7?slim=true) | | [github.com/aws/smithy-go](https://redirect.github.com/aws/smithy-go) | `v1.24.0` → `v1.24.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2fsmithy-go/v1.24.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2fsmithy-go/v1.24.0/v1.24.2?slim=true) | | [github.com/charmbracelet/lipgloss/v2](https://redirect.github.com/charmbracelet/lipgloss) | `v2.0.0-beta.3` → `v2.0.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fcharmbracelet%2flipgloss%2fv2/v2.0.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fcharmbracelet%2flipgloss%2fv2/v2.0.0-beta.3/v2.0.0?slim=true) | | [github.com/getsentry/sentry-go](https://redirect.github.com/getsentry/sentry-go) | `v0.42.0` → `v0.43.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgetsentry%2fsentry-go/v0.43.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgetsentry%2fsentry-go/v0.42.0/v0.43.0?slim=true) | | [github.com/goreleaser/goreleaser/v2](https://redirect.github.com/goreleaser/goreleaser) | `v2.13.3` → `v2.14.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fgoreleaser%2fgoreleaser%2fv2/v2.14.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fgoreleaser%2fgoreleaser%2fv2/v2.13.3/v2.14.1?slim=true) | | [github.com/harness/harness-go-sdk](https://redirect.github.com/harness/harness-go-sdk) | `v0.7.9` → `v0.7.12` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fharness%2fharness-go-sdk/v0.7.12?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fharness%2fharness-go-sdk/v0.7.9/v0.7.12?slim=true) | | [github.com/hashicorp/terraform-plugin-framework](https://redirect.github.com/hashicorp/terraform-plugin-framework) | `v1.17.0` → `v1.18.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fhashicorp%2fterraform-plugin-framework/v1.18.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fhashicorp%2fterraform-plugin-framework/v1.17.0/v1.18.0?slim=true) | | [github.com/hashicorp/terraform-plugin-go](https://redirect.github.com/hashicorp/terraform-plugin-go) | `v0.29.0` → `v0.30.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fhashicorp%2fterraform-plugin-go/v0.30.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fhashicorp%2fterraform-plugin-go/v0.29.0/v0.30.0?slim=true) | | [github.com/incu6us/goimports-reviser/v3](https://redirect.github.com/incu6us/goimports-reviser) | `v3.11.0` → `v3.12.6` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fincu6us%2fgoimports-reviser%2fv3/v3.12.6?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fincu6us%2fgoimports-reviser%2fv3/v3.11.0/v3.12.6?slim=true) | | [github.com/kaptinlin/jsonrepair](https://redirect.github.com/kaptinlin/jsonrepair) | `v0.2.8` → `v0.2.17` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fkaptinlin%2fjsonrepair/v0.2.17?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fkaptinlin%2fjsonrepair/v0.2.8/v0.2.17?slim=true) | | [github.com/micahhausler/aws-iam-policy](https://redirect.github.com/micahhausler/aws-iam-policy) | `v0.4.2` → `v0.4.3` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fmicahhausler%2faws-iam-policy/v0.4.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fmicahhausler%2faws-iam-policy/v0.4.2/v0.4.3?slim=true) | | [github.com/nats-io/nats.go](https://redirect.github.com/nats-io/nats.go) | `v1.48.0` → `v1.49.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fnats-io%2fnats.go/v1.49.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fnats-io%2fnats.go/v1.48.0/v1.49.0?slim=true) | | [github.com/openai/openai-go/v3](https://redirect.github.com/openai/openai-go) | `v3.21.0` → `v3.24.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fopenai%2fopenai-go%2fv3/v3.24.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fopenai%2fopenai-go%2fv3/v3.21.0/v3.24.0?slim=true) | | [github.com/riverqueue/river](https://redirect.github.com/riverqueue/river) | `v0.30.2` → `v0.31.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2friverqueue%2friver/v0.31.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2friverqueue%2friver/v0.30.2/v0.31.0?slim=true) | | [github.com/riverqueue/river/riverdriver/riverpgxv5](https://redirect.github.com/riverqueue/river) | `v0.30.2` → `v0.31.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2friverqueue%2friver%2friverdriver%2friverpgxv5/v0.31.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2friverqueue%2friver%2friverdriver%2friverpgxv5/v0.30.2/v0.31.0?slim=true) | | [github.com/riverqueue/river/rivertype](https://redirect.github.com/riverqueue/river) | `v0.30.2` → `v0.31.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2friverqueue%2friver%2frivertype/v0.31.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2friverqueue%2friver%2frivertype/v0.30.2/v0.31.0?slim=true) | | [github.com/stripe/stripe-go/v84](https://redirect.github.com/stripe/stripe-go) | `v84.3.0` → `v84.4.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fstripe%2fstripe-go%2fv84/v84.4.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fstripe%2fstripe-go%2fv84/v84.3.0/v84.4.0?slim=true) | | [github.com/zclconf/go-cty](https://redirect.github.com/zclconf/go-cty) | `v1.17.0` → `v1.18.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fzclconf%2fgo-cty/v1.18.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fzclconf%2fgo-cty/v1.17.0/v1.18.0?slim=true) | | [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) | [`v0.50.0` → `v0.51.0`](https://cs.opensource.google/go/x/net/+/refs/tags/v0.50.0...refs/tags/v0.51.0) | ![age](https://developer.mend.io/api/mc/badges/age/go/golang.org%2fx%2fnet/v0.51.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/golang.org%2fx%2fnet/v0.50.0/v0.51.0?slim=true) | | [google.golang.org/api](https://redirect.github.com/googleapis/google-api-go-client) | `v0.266.0` → `v0.269.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/google.golang.org%2fapi/v0.269.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/google.golang.org%2fapi/v0.266.0/v0.269.0?slim=true) | | [k8s.io/api](https://redirect.github.com/kubernetes/api) | `v0.35.1` → `v0.35.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/k8s.io%2fapi/v0.35.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/k8s.io%2fapi/v0.35.1/v0.35.2?slim=true) | | [k8s.io/apimachinery](https://redirect.github.com/kubernetes/apimachinery) | `v0.35.1` → `v0.35.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/k8s.io%2fapimachinery/v0.35.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/k8s.io%2fapimachinery/v0.35.1/v0.35.2?slim=true) | | [k8s.io/client-go](https://redirect.github.com/kubernetes/client-go) | `v0.35.1` → `v0.35.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/k8s.io%2fclient-go/v0.35.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/k8s.io%2fclient-go/v0.35.1/v0.35.2?slim=true) | | [k8s.io/component-base](https://redirect.github.com/kubernetes/component-base) | `v0.35.1` → `v0.35.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/k8s.io%2fcomponent-base/v0.35.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/k8s.io%2fcomponent-base/v0.35.1/v0.35.2?slim=true) | | [modernc.org/sqlite](https://gitlab.com/cznic/sqlite) | `v1.45.0` → `v1.46.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/modernc.org%2fsqlite/v1.46.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/modernc.org%2fsqlite/v1.45.0/v1.46.1?slim=true) | | riverqueue.com/riverui | `v0.14.0` → `v0.15.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/riverqueue.com%2friverui/v0.15.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/riverqueue.com%2friverui/v0.14.0/v0.15.0?slim=true) | | [sigs.k8s.io/controller-runtime/tools/setup-envtest](https://redirect.github.com/kubernetes-sigs/controller-runtime) | `v0.0.0-20260215011159-80d56e741406` → `v0.0.0-20260216173200-e4c1c38bcbdb` | ![age](https://developer.mend.io/api/mc/badges/age/go/sigs.k8s.io%2fcontroller-runtime%2ftools%2fsetup-envtest/v0.0.0-20260216173200-e4c1c38bcbdb?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/sigs.k8s.io%2fcontroller-runtime%2ftools%2fsetup-envtest/v0.0.0-20260215011159-80d56e741406/v0.0.0-20260216173200-e4c1c38bcbdb?slim=true) | | [sigs.k8s.io/controller-tools/cmd/controller-gen](https://redirect.github.com/kubernetes-sigs/controller-tools) | `v0.20.0` → `v0.20.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/sigs.k8s.io%2fcontroller-tools%2fcmd%2fcontroller-gen/v0.20.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/sigs.k8s.io%2fcontroller-tools%2fcmd%2fcontroller-gen/v0.20.0/v0.20.1?slim=true) | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/370) for more information. ## ⚠️ Warning These modules are almost certainly going to break everything. They do every time they update. If you update even one repo's OTEL modules, go will then pull in new versions due to [MVS](https://research.swtch.com/vgo-mvs) which will cause your repo to break. All [otel pull requests](https://redirect.github.com/pulls?q=is%3Aopen+is%3Apr+user%3Aovermindtech+archived%3Afalse+label%3Aobservability+) need to be merged basically at the same time, and after all of the modules have been updated to be compatible with each other. ## ⚠️ Warning These modules contain database migrations that need to be added manually to our atlas migrations. Check the contents of https://github.com/riverqueue/river/tree/master/rivermigrate/migration before merging this update. --- ### Release Notes
bufbuild/protovalidate-go (buf.build/go/protovalidate) ### [`v1.1.3`](https://redirect.github.com/bufbuild/protovalidate-go/releases/tag/v1.1.3) [Compare Source](https://redirect.github.com/bufbuild/protovalidate-go/compare/v1.1.2...v1.1.3) #### What's Changed - Fix a few godoc comments and update golangci-lint by [@​pkwarren](https://redirect.github.com/pkwarren) in [#​306](https://redirect.github.com/bufbuild/protovalidate-go/pull/306) - Bump the go group across 1 directory with 2 updates by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​308](https://redirect.github.com/bufbuild/protovalidate-go/pull/308) - Fix registry chain for pb.Map in NativeToValue by [@​rodaine](https://redirect.github.com/rodaine) in [#​309](https://redirect.github.com/bufbuild/protovalidate-go/pull/309) **Full Changelog**:
a-h/templ (github.com/a-h/templ) ### [`v0.3.1001`](https://redirect.github.com/a-h/templ/releases/tag/v0.3.1001) [Compare Source](https://redirect.github.com/a-h/templ/compare/v0.3.977...v0.3.1001) #### Changelog - [`2c505c0`](https://redirect.github.com/a-h/templ/commit/2c505c0) chore: add unit test to cover recent fix - [`4233429`](https://redirect.github.com/a-h/templ/commit/4233429) chore: bump compiler to Go 1.26 - [`1b9a429`](https://redirect.github.com/a-h/templ/commit/1b9a429) chore: bump deps in fiber example - [`54981db`](https://redirect.github.com/a-h/templ/commit/54981db) chore: bump docusaurus version - [`e606c30`](https://redirect.github.com/a-h/templ/commit/e606c30) chore: bump flake builder - [`66bc28b`](https://redirect.github.com/a-h/templ/commit/66bc28b) chore: bump gofiber example deps - [`95f88a6`](https://redirect.github.com/a-h/templ/commit/95f88a6) chore: bump to Go 1.25, update csrf example - [`916a243`](https://redirect.github.com/a-h/templ/commit/916a243) chore: bump version - [`45dda73`](https://redirect.github.com/a-h/templ/commit/45dda73) chore: fix test broken by merge - [`5ddd784`](https://redirect.github.com/a-h/templ/commit/5ddd784) chore: revert Nix bump to Go 1.26 because it breaks the golangci-lint package - [`4037d8a`](https://redirect.github.com/a-h/templ/commit/4037d8a) feat: add Range to BoolConstantAttribute nodes ([#​1340](https://redirect.github.com/a-h/templ/issues/1340)) - [`afb0034`](https://redirect.github.com/a-h/templ/commit/afb0034) feat: add Range to BooleanExpressionAttribute nodes ([#​1336](https://redirect.github.com/a-h/templ/issues/1336)) - [`c80f745`](https://redirect.github.com/a-h/templ/commit/c80f745) feat: add Range to ChildrenExpression nodes ([#​1337](https://redirect.github.com/a-h/templ/issues/1337)) - [`b0f5243`](https://redirect.github.com/a-h/templ/commit/b0f5243) feat: add Range to ConditionalAttribute nodes ([#​1338](https://redirect.github.com/a-h/templ/issues/1338)) - [`60fc376`](https://redirect.github.com/a-h/templ/commit/60fc376) feat: add Range to ConstantAttribute nodes ([#​1341](https://redirect.github.com/a-h/templ/issues/1341)) - [`b4e809e`](https://redirect.github.com/a-h/templ/commit/b4e809e) feat: add Range to SpreadAttributes nodes ([#​1335](https://redirect.github.com/a-h/templ/issues/1335)) - [`5824d4b`](https://redirect.github.com/a-h/templ/commit/5824d4b) feat: add TLS support to live reload proxy ([#​1345](https://redirect.github.com/a-h/templ/issues/1345)) - [`e9a940b`](https://redirect.github.com/a-h/templ/commit/e9a940b) feat: strip space from CSS classname rendering, closes [#​1074](https://redirect.github.com/a-h/templ/issues/1074) ([#​1346](https://redirect.github.com/a-h/templ/issues/1346)) - [`bdda41e`](https://redirect.github.com/a-h/templ/commit/bdda41e) fix: don't remove unaliased hyphenated imports if they're used ([#​1342](https://redirect.github.com/a-h/templ/issues/1342)) - [`c2ff8bb`](https://redirect.github.com/a-h/templ/commit/c2ff8bb) fix: issue 1253 ([#​1339](https://redirect.github.com/a-h/templ/issues/1339)) - [`cf6235a`](https://redirect.github.com/a-h/templ/commit/cf6235a) fix: proxy escaping characters ([#​1321](https://redirect.github.com/a-h/templ/issues/1321)) - [`d97730c`](https://redirect.github.com/a-h/templ/commit/d97730c) fix: support nushell for prettier, fixes [#​1266](https://redirect.github.com/a-h/templ/issues/1266) ([#​1343](https://redirect.github.com/a-h/templ/issues/1343)) - [`b666bd7`](https://redirect.github.com/a-h/templ/commit/b666bd7) fix: undefined variable in proxy test range loop ([#​1324](https://redirect.github.com/a-h/templ/issues/1324)) - [`be8271d`](https://redirect.github.com/a-h/templ/commit/be8271d) refactor: skip some more tests that require prettier
auth0/go-auth0 (github.com/auth0/go-auth0/v2) ### [`v2.6.0`](https://redirect.github.com/auth0/go-auth0/blob/HEAD/CHANGELOG.md#v260-2026-02-25) [Compare Source](https://redirect.github.com/auth0/go-auth0/compare/v2.5.0...v2.6.0) [Full Changelog](https://redirect.github.com/auth0/go-auth0/compare/v2.5.0...v2.6.0) **Added** - feat: add connection keys provisioning, SCIM config list, DPoP signing, and on-behalf-of token exchange support [#​706](https://redirect.github.com/auth0/go-auth0/pull/706) ([fern-api\[bot\]](https://redirect.github.com/apps/fern-api)) - feat: add WithTokenSource option for custom token management [#​705](https://redirect.github.com/auth0/go-auth0/pull/705) ([developerkunal](https://redirect.github.com/developerkunal)) **Fixed** - fix: clone http.DefaultClient in authentication.New to avoid mutating global state [#​704](https://redirect.github.com/auth0/go-auth0/pull/704) ([developerkunal](https://redirect.github.com/developerkunal))
aws/aws-sdk-go-v2 (github.com/aws/aws-sdk-go-v2) ### [`v1.41.2`](https://redirect.github.com/aws/aws-sdk-go-v2/blob/HEAD/CHANGELOG.md#Release-2025-06-26) [Compare Source](https://redirect.github.com/aws/aws-sdk-go-v2/compare/v1.41.1...v1.41.2) #### Module Highlights - `github.com/aws/aws-sdk-go-v2/service/deadline`: [v1.14.0](service/deadline/CHANGELOG.md#v1140-2025-06-26) - **Feature**: Added fields to track cumulative task retry attempts for steps and jobs - `github.com/aws/aws-sdk-go-v2/service/ec2`: [v1.227.0](service/ec2/CHANGELOG.md#v12270-2025-06-26) - **Feature**: This release adds support for OdbNetworkArn as a target in VPC Route Tables - `github.com/aws/aws-sdk-go-v2/service/iotmanagedintegrations`: [v1.1.0](service/iotmanagedintegrations/CHANGELOG.md#v110-2025-06-26) - **Feature**: Adding managed integrations APIs for IoT Device Management to onboard and control devices across different manufacturers, connectivity protocols and third party vendor clouds. APIs include managed thing operations, provisioning profile management, and cloud connector operations. - `github.com/aws/aws-sdk-go-v2/service/keyspaces`: [v1.19.0](service/keyspaces/CHANGELOG.md#v1190-2025-06-26) - **Feature**: This release provides change data capture (CDC) streams support through updates to the Amazon Keyspaces API. - `github.com/aws/aws-sdk-go-v2/service/keyspacesstreams`: [v1.0.0](service/keyspacesstreams/CHANGELOG.md#v100-2025-06-26) - **Release**: New AWS service client module - **Feature**: This release adds change data capture (CDC) streams support through the new Amazon Keyspaces Streams API. - `github.com/aws/aws-sdk-go-v2/service/kms`: [v1.41.2](service/kms/CHANGELOG.md#v1412-2025-06-26) - **Documentation**: This release updates AWS CLI examples for KMS APIs. - `github.com/aws/aws-sdk-go-v2/service/qbusiness`: [v1.27.0](service/qbusiness/CHANGELOG.md#v1270-2025-06-26) - **Feature**: Added support for App level authentication for QBusiness DataAccessor using AWS IAM Identity center Trusted Token issuer - `github.com/aws/aws-sdk-go-v2/service/workspaces`: [v1.58.0](service/workspaces/CHANGELOG.md#v1580-2025-06-26) - **Feature**: Updated modifyStreamingProperties to support PrivateLink VPC endpoints for directories
aws/smithy-go (github.com/aws/smithy-go) ### [`v1.24.2`](https://redirect.github.com/aws/smithy-go/releases/tag/v1.24.2): Release (2026-02-27) [Compare Source](https://redirect.github.com/aws/smithy-go/compare/v1.24.1...v1.24.2) ### Release (2026-02-27) #### General Highlights - **Dependency Update**: Bump minimum go version to 1.24. ### [`v1.24.1`](https://redirect.github.com/aws/smithy-go/blob/HEAD/CHANGELOG.md#Release-2026-02-20) [Compare Source](https://redirect.github.com/aws/smithy-go/compare/v1.24.0...v1.24.1) #### General Highlights - **Dependency Update**: Updated to the latest SDK module versions #### Module Highlights - `github.com/aws/smithy-go`: v1.24.1 - **Feature**: Add new middleware functions to get event stream output from middleware
charmbracelet/lipgloss (github.com/charmbracelet/lipgloss/v2) ### [`v2.0.0`](https://redirect.github.com/charmbracelet/lipgloss/releases/tag/v2.0.0) ![lipgloss-v2-block](https://redirect.github.com/user-attachments/assets/51264df0-cbf9-4885-9910-43ba4fdf1f3d) ### Do you think you can handle Lip Gloss v2? We’re really excited for you to try Lip Gloss v2! Read on for new features and a guide to upgrading. If you (or your LLM) just want the technical details, take a look at [Upgrade Guide](UPGRADE_GUIDE_V2.md). > \[!NOTE] > We take API changes seriously and strive to make the upgrade process as simple as possible. We believe the changes bring necessary improvements as well as pave the way for the future. If something feels way off, let us know. #### What’s new? The big changes are that Styles are now deterministic (λipgloss!) and you can be much more intentional with your inputs and outputs. Why does this matter? ##### Playing nicely with others v2 gives you precise control over I/O. One of the issues we saw with the Lip Gloss and [Bubble Tea](https://redirect.github.com/charmbracelet/bubbletea) v1s is that they could fight over the same inputs and outputs, producing lock-ups. The v2s now operate in lockstep. ##### Querying the right inputs and outputs In v1, Lip Gloss defaulted to looking at `stdin` and `stdout` when downsampling colors and querying for the background color. This was not always necessarily what you wanted. For example, if your application was writing to `stderr` while redirecting `stdout` to a file, the program would erroneously think output was not a TTY and strip colors. Lip Gloss v2 gives you control over this. ##### Going beyond localhost Did you know TUIs and CLIs can be served over the network? For example, [Wish](https://redirect.github.com/charmbracelet/wish) allows you to serve Bubble Tea and Lip Gloss over SSH. In these cases, you need to work with the input and output of the connected clients as opposed to `stdin` and `stdout`, which belong to the server. Lip Gloss v2 gives you flexibility around this in a more natural way. #### 🧋 Using Lip Gloss with Bubble Tea? Make sure you get all the latest v2s as they’ve been designed to work together. ```bash # Collect the whole set. go get charm.land/bubbletea/v2 go get charm.land/bubbles/v2 go get charm.land/lipgloss/v2 ``` #### 🐇 Quick upgrade If you don't have time for changes and just want to upgrade to Lip Gloss v2 as fast as possible? Here’s a quick guide: ##### Use the `compat` package The `compat` package provides adaptive colors, complete colors, and complete adaptive colors: ```go import "charm.land/lipgloss/v2/compat" // Before color := lipgloss.AdaptiveColor{Light: "#f1f1f1", Dark: "#cccccc"} // After color := compat.AdaptiveColor{Light: lipgloss.Color("#f1f1f1"), Dark: lipgloss.Color("#cccccc")} ``` `compat` works by looking at `stdin` and `stdout` on a global basis. Want to change the inputs and outputs? Knock yourself out: ```go import ( "charm.land/lipgloss/v2/compat" "github.com/charmbracelet/colorprofile" ) func init() { // Let’s use stderr instead of stdout. compat.HasDarkBackground = lipgloss.HasDarkBackground(os.Stdin, os.Stderr) compat.Profile = colorprofile.Detect(os.Stderr, os.Environ()) } ``` ##### Use the new Lip Gloss writer If you’re using Bubble Tea with Lip Gloss you can skip this step. If you're using Lip Gloss in a standalone fashion, however, you'll want to use `lipgloss.Println` (and `lipgloss.Printf` and so on) when printing your output: ```go s := someStyle.Render("Fancy Lip Gloss Output") // Before fmt.Println(s) // After lipgloss.Println(s) ``` Why? Because `lipgloss.Println` will automatically downsample colors based on the environment. ##### That’s it! Yep, you’re done. All this said, we encourage you to read on to get the full benefit of v2. #### 👀 What’s changing? Only a couple main things that are changing in Lip Gloss v2: - Color downsampling in non-Bubble-Tea uses cases is now a manual proccess (don't worry, it's easy) - Background color detection and adaptive colors are manual, and intentional (but optional) ##### 🪄 Downsampling colors with a writer One of the best things about Lip Gloss is that it can automatically downsample colors to the best available profile, stripping colors (and ANSI) entirely when output is not a TTY. If you're using Lip Gloss with Bubble Tea there's nothing to do here: downsampling is built into Bubble Tea v2. If you're not using Bubble Tea you now need to use a writer to downsample colors. Lip Gloss writers are a drop-in replacement for the usual functions found in the `fmt` package: ```go s := someStyle.Render("Hello!") // Downsample and print to stdout. lipgloss.Println(s) // Render to a variable. downsampled := lipgloss.Sprint(s) // Print to stderr. lipgloss.Fprint(os.Stderr, s) ``` ##### 🌛 Background color detection and adaptive colors Rendering different colors depending on whether the terminal has a light or dark background is an awesome power. Lip Gloss v2 gives you more control over this progress. This especially matters when input and output are not `stdin` and `stdout`. If that *doesn’t* matter to you and you're only working with `stdout` you skip this via [`compat` above](#quick-upgrade), though we encourage you to explore this new functionality. ##### With Bubble Tea In Bubble Tea, request the background color, listen for a `BackgroundColorMsg` in your update, and respond accordingly. ```go // Query for the background color. func (m model) Init() tea.Cmd { return tea.RequestBackgroundColor } // Listen for the response and initialize your styles accordigly. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.BackgroundColorMsg: // Initialize your styles now that you know the background color. m.styles = newStyles(msg.IsDark()) return m, nil } } type styles { myHotStyle lipgloss.Style } func newStyles(bgIsDark bool) (s styles) { lightDark := lipgloss.LightDark(bgIsDark) // just a helper function return styles{ myHotStyle := lipgloss.NewStyle().Foreground(lightDark("#f1f1f1", "#​333333")) } } ``` ##### Standalone If you're not using Bubble Tea you simply can perform the query manually: ```go // Detect the background color. Notice we're writing to stderr. hasDarkBG, err := lipgloss.HasDarkBackground(os.Stdin, os.Stderr) if err != nil { log.Fatal("Oof:", err) } // Create a helper for choosing the appropriate color. lightDark := lipgloss.LightDark(hasDarkBG) // Declare some colors. thisColor := lightDark("#C5ADF9", "#​864EFF") thatColor := lightDark("#​37CD96", "#​22C78A") // Render some styles. a := lipgloss.NewStyle().Foreground(thisColor).Render("this") b := lipgloss.NewStyle().Foreground(thatColor).Render("that") // Print to stderr. lipgloss.Fprintf(os.Stderr, "my fave colors are %s and %s...for now.", a, b) ``` #### 🥕 Other stuff ##### Colors are now `color.Color` `lipgloss.Color()` now produces an idiomatic `color.Color`, whereas before colors were type `lipgloss.TerminalColor`. Generally speaking, this is more of an implementation detail, but it’s worth noting the structural differences. ```go // Before type TerminalColor interface{/* ... */} type Color string // After func Color(string) color.Color type RGBColor struct{R, G, B uint8} func LightDark(isDark bool) LightDarkFunc type LightDarkFunc func(light, dark color.Color) color.Color func Complete(colorprofile.Profile) CompleteFunc type CompleteFunc func(ansi, ansi256, truecolor color.Color) color.Color ```
#### Changelog ##### New! * b259725e46e9fbb2af6673d74f26917ed42df370: feat(blending): early return when steps <= num stops (#​566) (@​lrstanley) * 71dd8ee66ac1f4312844a792952789102513c9c5: feat(borders): initial border blend implementation (#​560) (@​lrstanley) * 2166ce88ec1cca66e8a820a86baafd7cfd34bcd0: feat(canvas): accept any type as layer content (@​aymanbagabas) * 0303864674b37235e99bc14cd4da17c409ec448e: feat(colors): refactor colors sub-package into root package (@​lrstanley) * 9c86c1f950fbfffd6c56a007de6bd3e61d67a1ea: feat(colors): switch from int to float64 for inputs (@​lrstanley) * 0334bb4562ca1f72a684c1c2a63c848ac21fffc6: feat(tree): support width and indenter styling (#​446) (@​dlvhdr) * 9a771f5a242df0acf862c7acd72124469eb4635a: feat: BlendLinear* -> Blend* (@​lrstanley) * 34443e82a7ddcbe37b9dc0d69b84385e400b8a5c: feat: add brightness example, misc example tweaks (@​lrstanley) * c95c5f3c5b27360d344bf82736a8ce9257aaf71e: feat: add hyperlink support (#​473) (@​aymanbagabas) * 5e542b8c69a0f20ea62b2caa422bbee5337fbb48: feat: add underline style and color (@​aymanbagabas) * d3032608aa74f458a7330e17cc304f1ebb5fa1b9: feat: add wrap implementation preserving styles and links (#​582) (@​aymanbagabas) * 7bf18447c8729839ca7e79aa3ba9aa00ecb8f963: feat: further simplify colors in examples (@​lrstanley) * 27a8cf99a81d1bd5ab875cd773ac8647320b02ba: feat: implement uv Drawable for Canvas and Layer (@​aymanbagabas) * c4c08fc4f8a107b00bc54407ad9094b9642dd103: feat: implement uv.Drawable for *Layer (#​607) (@​ayn2op) * 18b4bb86c515f93eede5720fe66b0d9ba83fa489: feat: initial implementation of color blending & brightness helpers (@​lrstanley) * 63610090044b782caa8ce8b1b53cc81b98264eaa: feat: update examples/layout to use colors.BlendLinear1D (@​lrstanley) * de4521b8baa33c49a96e9458e9d9213c7ba407bd: feat: update examples/list/sublist to use colors.BlendLinear1D (@​lrstanley) * 1b3716cc53b5cc29c2b1b0c655a684b797fef075: feat: use custom hex parsing for increased perf (@​lrstanley) ##### Fixed * 06ca257e382fa107afcfe147c9cda836b3cdb4be: fix(canvas): Hit method should return Layer ID as string instead of *Layer (@​aymanbagabas) * d1fa8790efbd70df8b0dd8bd139434f3ac6e063b: fix(canvas): handle misc edge cases (#​588) (@​lrstanley) * 7869489d8971e2e3a8de8e0a4a1e1dfe4895a352: fix(canvas): simplify Render handling (@​aymanbagabas) * 68f38bdee72b769ff9c137a4097d9e64d401b703: fix(ci): use local golangci config (@​aymanbagabas) * ff11224963a33f6043dfb3408e67c7fea7759f34: fix(color): update deprecated types (@​aymanbagabas) * 3f659a836c78f6ad31f5652571007cb4ab9d1eb8: fix(colors): update examples to use new method locations (@​lrstanley) * 3248589b24c9894694be6d1862817acb77e119cc: fix(layers): allow recursive rendering for layers that only contain children (#​589) (@​lrstanley) * 6c33b19c3f0a1e7d50ce9028ef4bda3ca631cd68: fix(lint): remove nolint:exhaustive comments and ignore var-naming rule for revive (@​aymanbagabas) * d267651963ad3ba740b30ecf394d7a5ef86704fc: fix(style): use alias for Underline type from ansi package (@​aymanbagabas) * 76690c6608346fc7ef09db388ee82feaa7920630: fix(table): fix wrong behavior of headers regarding margins (#​513) (@​andreynering) * 41ff0bf215ea2a444c5161d0bd7fa38b4a70af27: fix(terminal): switch to uv.NewCancelReader for Windows compatibility (@​aymanbagabas) * 5d69c0e790f24cbfaa94f8f8b2b64d1bb926c96d: fix: ensure we strip out \r\n from strings when getting lines (@​aymanbagabas) * 2e570c2690b61bac103e7eef9da917d1dfc6512d: fix: linear-2d example (@​lrstanley) * 0d6a022f7d075e14d61a755b3e9cab9d97519f21: fix: lint issues (@​aymanbagabas) * 832bc9d6b9d209e002bf1131938ffe7dbba07652: fix: prevent infinite loop with zero-width whitespace chars (#​108) (#​604) (@​calobozan) * 354e70d6d0762e6a54cfc45fe8d019d6087a4c00: fix: rename underline constants to be consistent with other style properties (@​raphamorim) ##### Docs * 60df47f8000b6cb5dfec46af37bceb2c9050bef0: docs(readme): cleanup badges (@​meowgorithm) * 881a1ffc54b6afb5f22ead143d10f8dce05e7e66: docs(readme): update art (@​meowgorithm) * ee74a03efa8363cf3b17ee7a128b9825c8f3791e: docs(readme): update footer art and copyright date (@​meowgorithm) * 8863cc06da67b8ef9f4b6f80c567738fa53bd090: docs(readme): update header image (@​meowgorithm) * 4e8ca2d9f045d6bca78ee0150420e26cda8bcccf: docs: add underline styles and colors caveats (@​aymanbagabas) * a8cfc26d7de7bdb335a8c7c2f0c8fc4f18ea8993: docs: add v2 upgrade and changes guide (#​611) (@​aymanbagabas) * 454007a0ad4e8b60afc1f6fdc3e3424e4d3a4c16: docs: update comments in for GetPaddingChar and GetMarginChar (@​aymanbagabas) * 95f30dbdc90cc409e8645de4bd2296a33ba37c70: docs: update mascot header image (@​aymanbagabas) * a06a847849dbd1726c72047a98ab8cce0f73a65f: docs: update readme in prep for v2 (#​613) (@​aymanbagabas) ##### Other stuff * 5ca0343ec7be2e85521e79734f4392cdb19e4949: Fix(table): BorderRow (#​514) (@​bashbunni) * f2d1864a58cd455ca118e04123feae177d7d2eef: Improve performance of maxRuneWidth (#​592) (@​clipperhouse) * 10c048e361129dd601eb6ff8c0c2458814291156: Merge v2-uv-canvas into v2-exp (@​aymanbagabas) * d02a007bb19e14f6bf351ed71a47beb6bee9cae3: ci: sync dependabot config (#​521) (@​charmcli) * 8708a8925b60c610e68b9aa6e509ebd513a8244e: ci: sync dependabot config (#​561) (@​charmcli) * 7d1b622c64d1a68cdc94b30864ae5ec3e6abc2dd: ci: sync dependabot config (#​572) (@​charmcli) * 19a4b99cb3bbbd2ab3079adc500faa1875da87e8: ci: sync golangci-lint config (@​aymanbagabas) * a6c079dc8a3fc6e68a00214a767627ec8447adb5: ci: sync golangci-lint config (@​aymanbagabas) * 350edde4903bcc2eee5a8ce1552dd90c3b89c125: ci: sync golangci-lint config (#​553) (@​github-actions[bot]) * 1e3ee3483a907facd98ca0a56f6694a0e9365f26: ci: sync golangci-lint config (#​598) (@​github-actions[bot]) * e729228ac14e63057e615a2241ce4303d59fef08: lint: fix lint for newer go versions (#​540) (@​andreynering) * 66093c8cf3b79596597c1e39fd4c67a954010fb3: perf: remove allocations from getFirstRuneAsString (#​578) (@​clipperhouse) * ad876c4132d61951d091a1a535c27237f6a90ad6: refactor: new Canvas, Compositor, and Layer API (#​591) (@​aymanbagabas) * 3aae2866142214f5b8ce9cbfc1939645928dcb8f: refactor: update imports to use charm.land domain (@​aymanbagabas)
#### 🌈 Feedback That's a wrap! Feel free to reach out, ask questions, and let us know how it's going. We'd *love* to know what you think. - [Discord](https://charm.land/discord) - [Matrix](https://charm.land/matrix) - [Email](mailto:vt100@​charm.land) *** Part of [Charm](https://charm.land). The Charm logo Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
getsentry/sentry-go (github.com/getsentry/sentry-go) ### [`v0.43.0`](https://redirect.github.com/getsentry/sentry-go/releases/tag/v0.43.0): 0.43.0 [Compare Source](https://redirect.github.com/getsentry/sentry-go/compare/v0.42.0...v0.43.0) ##### Breaking Changes 🛠 - Add support for go 1.26 by [@​giortzisg](https://redirect.github.com/giortzisg) in [#​1193](https://redirect.github.com/getsentry/sentry-go/pull/1193) - bump minimum supported go version to 1.24 - change type signature of attributes for Logs and Metrics. by [@​giortzisg](https://redirect.github.com/giortzisg) in [#​1205](https://redirect.github.com/getsentry/sentry-go/pull/1205) - users are not supposed to modify Attributes directly on the Log/Metric itself, but this is still is a breaking change on the type. - Send uint64 overflowing attributes as numbers. by [@​giortzisg](https://redirect.github.com/giortzisg) in [#​1198](https://redirect.github.com/getsentry/sentry-go/pull/1198) - The SDK was converting overflowing uint64 attributes to strings for slog and logrus integrations. To eliminate double types for these attributes, the SDK now sends the overflowing attribute as is, and lets the server handle the overflow appropriately. - It is expected that overflowing unsigned integers would now get dropped, instead of converted to strings. ##### New Features ✨ - Add zap logging integration by [@​giortzisg](https://redirect.github.com/giortzisg) in [#​1184](https://redirect.github.com/getsentry/sentry-go/pull/1184) - Log specific message for RequestEntityTooLarge by [@​giortzisg](https://redirect.github.com/giortzisg) in [#​1185](https://redirect.github.com/getsentry/sentry-go/pull/1185) ##### Bug Fixes 🐛 - Improve otel span map cleanup performance by [@​giortzisg](https://redirect.github.com/giortzisg) in [#​1200](https://redirect.github.com/getsentry/sentry-go/pull/1200) - Ensure correct signal delivery on multi-client setups by [@​giortzisg](https://redirect.github.com/giortzisg) in [#​1190](https://redirect.github.com/getsentry/sentry-go/pull/1190) ##### Internal Changes 🔧 ##### Deps - Bump golang.org/x/crypto to 0.48.0 by [@​giortzisg](https://redirect.github.com/giortzisg) in [#​1196](https://redirect.github.com/getsentry/sentry-go/pull/1196) - Use go1.24.0 by [@​giortzisg](https://redirect.github.com/giortzisg) in [#​1195](https://redirect.github.com/getsentry/sentry-go/pull/1195) - Bump github.com/gofiber/fiber/v2 from 2.52.9 to 2.52.11 in /fiber by [@​dependabot](https://redirect.github.com/dependabot) in [#​1191](https://redirect.github.com/getsentry/sentry-go/pull/1191) - Bump getsentry/craft from 2.19.0 to 2.20.1 by [@​dependabot](https://redirect.github.com/dependabot) in [#​1187](https://redirect.github.com/getsentry/sentry-go/pull/1187) ##### Other - Add omitzero and remove custom serialization by [@​giortzisg](https://redirect.github.com/giortzisg) in [#​1197](https://redirect.github.com/getsentry/sentry-go/pull/1197) - Rename Telemetry Processor components by [@​giortzisg](https://redirect.github.com/giortzisg) in [#​1186](https://redirect.github.com/getsentry/sentry-go/pull/1186)
goreleaser/goreleaser (github.com/goreleaser/goreleaser/v2) ### [`v2.14.1`](https://redirect.github.com/goreleaser/goreleaser/releases/tag/v2.14.1) [Compare Source](https://redirect.github.com/goreleaser/goreleaser/compare/v2.14.0...v2.14.1) #### Announcement Read the official announcement: [Announcing GoReleaser v2.14](https://goreleaser.com/blog/goreleaser-v2.14/). #### Changelog ##### Security updates - [`6bce54f`](https://redirect.github.com/goreleaser/goreleaser/commit/6bce54f02c901c4e42e699d35e66b4d82f2ce162): sec(deps): update filippo.io/edwards25519 ([@​caarlos0](https://redirect.github.com/caarlos0)) ##### Bug fixes - [`6e813e9`](https://redirect.github.com/goreleaser/goreleaser/commit/6e813e9a69f48c67999dbef151b3f79f13e04756): fix(docker/v2): output in error ([@​caarlos0](https://redirect.github.com/caarlos0)) - [`4621880`](https://redirect.github.com/goreleaser/goreleaser/commit/46218808ad593c62b58b7bd37dd516501413cb62): fix(tmpl): rename toSlice to list ([@​caarlos0](https://redirect.github.com/caarlos0)) - [`c174134`](https://redirect.github.com/goreleaser/goreleaser/commit/c1741345b51d0c19f3f26a6af329100f28593b68): fix(tmpl): slice -> toSlice ([@​caarlos0](https://redirect.github.com/caarlos0)) ##### Documentation updates - [`8d53236`](https://redirect.github.com/goreleaser/goreleaser/commit/8d53236324a057e06f6f9a81803df6c13bd29b3d): docs: announce v2.14 ([@​caarlos0](https://redirect.github.com/caarlos0)) - [`c5c6637`](https://redirect.github.com/goreleaser/goreleaser/commit/c5c6637a850dcb9d68b6b23465bba94d3440e5a8): docs: fix gemfury links ([@​caarlos0](https://redirect.github.com/caarlos0)) - [`79922a2`](https://redirect.github.com/goreleaser/goreleaser/commit/79922a277cfe9d9ab9cf8814eb9f1503f6417a02): docs: fix more divider ([@​caarlos0](https://redirect.github.com/caarlos0)) - [`fe23919`](https://redirect.github.com/goreleaser/goreleaser/commit/fe23919a06c53f17a794680ba351df6d19763f33): docs: fix syntax ([@​caarlos0](https://redirect.github.com/caarlos0)) - [`0c1ac6d`](https://redirect.github.com/goreleaser/goreleaser/commit/0c1ac6dd222c2f026026fa334d1974cf21a95b22): docs: fmt ([@​caarlos0](https://redirect.github.com/caarlos0)) - [`ce778c3`](https://redirect.github.com/goreleaser/goreleaser/commit/ce778c3d2147417769275463f94e1931ca2de390): docs: typo ([@​caarlos0](https://redirect.github.com/caarlos0)) - [`63e1434`](https://redirect.github.com/goreleaser/goreleaser/commit/63e143478d2c6f36ea3571a19dc77b8131008595): docs: update github action ([@​caarlos0](https://redirect.github.com/caarlos0)) ##### Other work - [`d525fe7`](https://redirect.github.com/goreleaser/goreleaser/commit/d525fe7b7b64047693acf1973d3ce1818907a9ce): chore: remove unused file ([@​caarlos0](https://redirect.github.com/caarlos0)) - [`fd46e47`](https://redirect.github.com/goreleaser/goreleaser/commit/fd46e47b69a1d7014f5852513c6ceb269118d0a3): fix ([@​caarlos0](https://redirect.github.com/caarlos0)) **Full Changelog**: #### Helping out This release is only possible thanks to **all** the support of some **awesome people**! Want to be one of them? You can [sponsor](https://goreleaser.com/sponsors/), get a [Pro License](https://goreleaser.com/pro) or [contribute with code](https://goreleaser.com/contributing). #### Where to go next? - Find examples and commented usage of all options in our [website](https://goreleaser.com/intro/). - Reach out on [Discord](https://discord.gg/RGEBtg8vQ6) and [Twitter](https://twitter.com/goreleaser)! 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). --- > [!NOTE] > **Medium Risk** > Broad Go module upgrades (cloud/AWS/Auth0/K8s/River/Sentry/etc.) can introduce subtle compile/runtime behavior changes despite no intentional app-logic edits. Generated `templ` output is re-vended, so mismatched tool versions or formatting differences could cause CI/build churn. > > **Overview** > **Dependency/tooling refresh.** Updates `go.mod`/`go.sum` with a wide set of Go module bumps (including Google Cloud, AWS SDK v2, Auth0, River/RiverUI, K8s libs, `golang.org/x/*`, etc.) and adds a new indirect `github.com/go-json-experiment/json` requirement. > > **Aligns `templ` across environments.** Pins `github.com/a-h/templ` and `templ` CLI to `v0.3.1001` in CI workflows and Docker build images/devcontainer, and re-generates committed `*_templ.go` files in `services/api-server/area51` and `services/gateway/area51` to match the new generator version. > > **Dev tooling bumps.** Updates several Go tool installs in the devcontainer (e.g., `goimports-reviser`, `setup-envtest`, `controller-gen`, `goreleaser`). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit be9943631e180b02b0893e8b6e940abd3dfd5e10. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Elliot Waddington GitOrigin-RevId: 1da2a88d783e8bf5b7fdbbbdf2c3aa1b1e5f8f2b --- go.mod | 160 ++++++++++++++--------------- go.sum | 316 ++++++++++++++++++++++++++++----------------------------- 2 files changed, 239 insertions(+), 237 deletions(-) diff --git a/go.mod b/go.mod index 272bd74e..550ffca3 100644 --- a/go.mod +++ b/go.mod @@ -11,21 +11,21 @@ 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-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 + buf.build/go/protovalidate v1.1.3 + cloud.google.com/go/aiplatform v1.118.0 + cloud.google.com/go/auth v0.18.2 + cloud.google.com/go/bigquery v1.74.0 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 v1.55.0 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 + cloud.google.com/go/dataproc/v2 v2.16.0 cloud.google.com/go/filestore v1.10.3 cloud.google.com/go/functions v1.19.7 cloud.google.com/go/iam v1.5.3 - cloud.google.com/go/kms v1.25.0 + cloud.google.com/go/kms v1.26.0 cloud.google.com/go/logging v1.13.2 cloud.google.com/go/monitoring v1.24.3 cloud.google.com/go/networksecurity v0.11.0 @@ -57,43 +57,43 @@ 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/a-h/templ v0.3.1001 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-auth0/v2 v2.6.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 - github.com/aws/aws-sdk-go-v2/credentials v1.19.7 - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 - github.com/aws/aws-sdk-go-v2/service/apigateway v1.38.4 - github.com/aws/aws-sdk-go-v2/service/autoscaling v1.64.0 - github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.0 - 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.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.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 - github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 - 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.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/aws/aws-sdk-go-v2 v1.41.2 + github.com/aws/aws-sdk-go-v2/config v1.32.10 + github.com/aws/aws-sdk-go-v2/credentials v1.19.10 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 + github.com/aws/aws-sdk-go-v2/service/apigateway v1.38.5 + github.com/aws/aws-sdk-go-v2/service/autoscaling v1.64.1 + github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.1 + github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.55.0 + github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.12 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.0 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.293.0 + github.com/aws/aws-sdk-go-v2/service/ecs v1.73.0 + github.com/aws/aws-sdk-go-v2/service/efs v1.41.11 + github.com/aws/aws-sdk-go-v2/service/eks v1.80.1 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.20 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.7 + github.com/aws/aws-sdk-go-v2/service/iam v1.53.3 + github.com/aws/aws-sdk-go-v2/service/kms v1.50.1 + github.com/aws/aws-sdk-go-v2/service/lambda v1.88.1 + github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.4 + github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.5 + github.com/aws/aws-sdk-go-v2/service/rds v1.116.1 + github.com/aws/aws-sdk-go-v2/service/route53 v1.62.2 + github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 + github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.2 + github.com/aws/aws-sdk-go-v2/service/sns v1.39.12 + github.com/aws/aws-sdk-go-v2/service/sqs v1.42.22 + github.com/aws/aws-sdk-go-v2/service/ssm v1.68.1 + github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 + github.com/aws/smithy-go v1.24.2 github.com/bombsimon/logrusr/v4 v4.1.0 github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 github.com/brianvoe/gofakeit/v7 v7.14.0 @@ -103,7 +103,7 @@ require ( 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/getsentry/sentry-go v0.42.0 + github.com/getsentry/sentry-go v0.43.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 @@ -112,34 +112,34 @@ 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.9 + github.com/harness/harness-go-sdk v0.7.12 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-20260224005459-813a97530220 - github.com/hashicorp/terraform-plugin-framework v1.17.0 - github.com/hashicorp/terraform-plugin-go v0.29.0 + github.com/hashicorp/terraform-plugin-framework v1.18.0 + github.com/hashicorp/terraform-plugin-go v0.30.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/kaptinlin/jsonrepair v0.2.17 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/micahhausler/aws-iam-policy v0.4.3 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/nats.go v1.49.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/openai/openai-go/v3 v3.24.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 @@ -147,9 +147,9 @@ require ( 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/river v0.31.0 + github.com/riverqueue/river/riverdriver/riverpgxv5 v0.31.0 + github.com/riverqueue/river/rivertype v0.31.0 github.com/riverqueue/rivercontrib/otelriver v0.7.0 github.com/rs/cors v1.11.1 github.com/samber/slog-logrus/v2 v2.5.3 @@ -161,13 +161,13 @@ require ( 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/stripe/stripe-go/v84 v84.4.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 + github.com/zclconf/go-cty v1.18.0 go.etcd.io/bbolt v1.4.3 go.opentelemetry.io/contrib/detectors/aws/ec2/v2 v2.0.0-20250901115419-474a7992e57c go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 @@ -180,24 +180,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.50.0 + golang.org/x/net v0.51.0 golang.org/x/oauth2 v0.35.0 golang.org/x/sync v0.19.0 golang.org/x/text v0.34.0 gonum.org/v1/gonum v0.17.0 - google.golang.org/api v0.266.0 + google.golang.org/api v0.269.0 google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 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.1 - k8s.io/apimachinery v0.35.1 - k8s.io/client-go v0.35.1 - k8s.io/component-base v0.35.1 - modernc.org/sqlite v1.45.0 - riverqueue.com/riverui v0.14.0 + k8s.io/api v0.35.2 + k8s.io/apimachinery v0.35.2 + k8s.io/client-go v0.35.2 + k8s.io/component-base v0.35.2 + modernc.org/sqlite v1.46.1 + riverqueue.com/riverui v0.15.0 sigs.k8s.io/controller-runtime v0.23.1 sigs.k8s.io/kind v0.31.0 ) @@ -234,19 +234,19 @@ require ( 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 + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.18 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // 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 @@ -283,7 +283,7 @@ require ( 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/fatih/color v1.18.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 @@ -307,7 +307,7 @@ require ( 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/cel-go v0.27.0 // 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 @@ -318,7 +318,7 @@ require ( 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/googleapis/enterprise-certificate-proxy v0.3.12 // 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 @@ -368,7 +368,7 @@ require ( 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-colorable v0.1.14 // 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 @@ -418,8 +418,8 @@ require ( 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/riverqueue/river/riverdriver v0.31.0 // indirect + github.com/riverqueue/river/rivershared v0.31.0 // 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 @@ -486,7 +486,7 @@ require ( 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 + golang.org/x/mod v0.33.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.40.0 // indirect @@ -514,3 +514,5 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.2 sigs.k8s.io/yaml v1.6.0 // indirect ) + +require github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect diff --git a/go.sum b/go.sum index 0b47b545..35d42840 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ 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-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= +buf.build/go/protovalidate v1.1.3 h1:m2GVEgQWd7rk+vIoAZ+f0ygGjvQTuqPQapBBdcpWVPE= +buf.build/go/protovalidate v1.1.3/go.mod h1:9XIuohWz+kj+9JVn3WQneHA5LZP50mjvneZMnbLkiIE= 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= @@ -27,22 +27,22 @@ 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.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/aiplatform v1.118.0 h1:c0HD2VpKurq/H+k5/EuZt9kA5s7mfSce4DM/Mc4hXag= +cloud.google.com/go/aiplatform v1.118.0/go.mod h1:27DcZJbaxFntewF6O0HojDE1B8JQOGKYopNjwoICFdI= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= 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/bigquery v1.74.0 h1:Q6bAMv+eyvufOpIrfrYxhM46qq1D3ZQTdgUDQqKS+n8= +cloud.google.com/go/bigquery v1.74.0/go.mod h1:iViO7Cx3A/cRKcHNRsHB3yqGAMInFBswrE9Pxazsc90= 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 v1.55.0 h1:1roY8Wqzi8EgDPFJ8SI2v+TI7DodHNn94xQ4fvx10XU= +cloud.google.com/go/compute v1.55.0/go.mod h1:fMFC0mRv+fW2ISg7M3tpDfpZ+kkrHpC/ImNFRCYiNK0= 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= @@ -52,8 +52,8 @@ cloud.google.com/go/datacatalog v1.26.1 h1:bCRKA8uSQN8wGW3Tw0gwko4E9a64GRmbW1nCb cloud.google.com/go/datacatalog v1.26.1/go.mod h1:2Qcq8vsHNxMDgjgadRFmFG47Y+uuIVsyEGUrlrKEdrg= cloud.google.com/go/dataplex v1.28.0 h1:rROI3iqMVI9nXT701ULoFRETQVAOAPC3mPSWFDxXFl0= 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/dataproc/v2 v2.16.0 h1:0g2hnjlQ8SQTnNeu+Bqqa61QPssfSZF3t+9ldRmx+VQ= +cloud.google.com/go/dataproc/v2 v2.16.0/go.mod h1:HlzFg8k1SK+bJN3Zsy2z5g6OZS1D4DYiDUgJtF0gJnE= 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= @@ -61,8 +61,8 @@ cloud.google.com/go/functions v1.19.7 h1:7LcOD18euIVGRUPaeCmgO6vfWSLNIsi6STWRQcd cloud.google.com/go/functions v1.19.7/go.mod h1:xbcKfS7GoIcaXr2FSwmtn9NXal1JR4TV6iYZlgXffwA= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/kms v1.25.0 h1:gVqvGGUmz0nYCmtoxWmdc1wli2L1apgP8U4fghPGSbQ= -cloud.google.com/go/kms v1.25.0/go.mod h1:XIdHkzfj0bUO3E+LvwPg+oc7s58/Ns8Nd8Sdtljihbk= +cloud.google.com/go/kms v1.26.0 h1:cK9mN2cf+9V63D3H1f6koxTatWy39aTI/hCjz1I+adU= +cloud.google.com/go/kms v1.26.0/go.mod h1:pHKOdFJm63hxBsiPkYtowZPltu9dW0MWvBa6IA4HM58= cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= @@ -186,8 +186,8 @@ 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/a-h/templ v0.3.1001 h1:yHDTgexACdJttyiyamcTHXr2QkIeVF1MukLy44EAhMY= +github.com/a-h/templ v0.3.1001/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= @@ -224,96 +224,96 @@ 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.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-auth0/v2 v2.6.0 h1:KCoLxTcH8qXPYbwKZxxFrL/6P+P+Zc58BQPL6w0Kt30= +github.com/auth0/go-auth0/v2 v2.6.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= -github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= -github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= -github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= -github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= +github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= +github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c= +github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= +github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= +github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= -github.com/aws/aws-sdk-go-v2/service/apigateway v1.38.4 h1:V8gcFwJPP3eXZXpeui+p97JmO7WtCkQlEAHrE6Kyt0k= -github.com/aws/aws-sdk-go-v2/service/apigateway v1.38.4/go.mod h1:iJF5UdwkFue/YuUGCFsCCdT3SBMUx0s+h5TNi0Sz+qg= -github.com/aws/aws-sdk-go-v2/service/autoscaling v1.64.0 h1:s92jPptCu97RNwU1yF3jD4ahLZrQ0QkUIvrn464rQ2A= -github.com/aws/aws-sdk-go-v2/service/autoscaling v1.64.0/go.mod h1:8O5Pj92iNpfw/Fa7WdHbn6YiEjDoVdutz+9PGRNoP3Y= -github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.0 h1:RUQqU9L1LnFJ+9t5hsSB7GI6dVvJDCnG4WgRlDeHK6E= -github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.0/go.mod h1:9Hd/cqshF4zl13KGLkWtRfITbvKR6m6FZHwhL2BYDSY= -github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.53.1 h1:ElB5x0nrBHgQs+XcpQ1XJpSJzMFCq6fDTpT6WQCWOtQ= -github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.53.1/go.mod h1:Cj+LUEvAU073qB2jInKV6Y0nvHX0k7bL7KAga9zZ3jw= -github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.11 h1:3+DkKJAq5VVqPNu3eT6j0UchZDjDsNeqFNAqsomMPDc= -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.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.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= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.6/go.mod h1:oJRLDix51wqBDlP9dv+blFkvvf7HESolQz5cdhdmV4A= -github.com/aws/aws-sdk-go-v2/service/iam v1.53.2 h1:62G6btFUwAa5uR5iPlnlNVAM0zJSLbWgDfKOfUC7oW4= -github.com/aws/aws-sdk-go-v2/service/iam v1.53.2/go.mod h1:av9clChrbZbJ5E21msSsiT2oghl2BJHfQGhCkXmhyu8= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.17 h1:Nhx/OYX+ukejm9t/MkWI8sucnsiroNYNGb5ddI9ungQ= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.17/go.mod h1:AjmK8JWnlAevq1b1NBtv5oQVG4iqnYXUufdgol+q9wg= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= -github.com/aws/aws-sdk-go-v2/service/kms v1.49.5 h1:DKibav4XF66XSeaXcrn9GlWGHos6D/vJ4r7jsK7z5CE= -github.com/aws/aws-sdk-go-v2/service/kms v1.49.5/go.mod h1:1SdcmEGUEQE1mrU2sIgeHtcMSxHuybhPvuEPANzIDfI= -github.com/aws/aws-sdk-go-v2/service/lambda v1.88.0 h1:u66DMbJWDFXs9458RAHNtq2d0gyqcZFV4mzRwfjM358= -github.com/aws/aws-sdk-go-v2/service/lambda v1.88.0/go.mod h1:ogjbkxFgFOjG3dYFQ8irC92gQfpfMDcy1RDKNSZWXNU= -github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.3 h1:Fobn9IdJv8lgpGv5BYR5m3sFwlMctKgKE9rMRKVKpIQ= -github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.3/go.mod h1:1Yhak+i7rIt8Yq2lWViNXI4zoMufmqqjR89vNwgzafw= -github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.4 h1:J38JaWrNRBxSU/nrrC92/jqGVl07RAdGXM9GvwtdQqE= -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.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= -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= -github.com/aws/aws-sdk-go-v2/service/sns v1.39.11/go.mod h1:hdZDKzao0PBfJJygT7T92x2uVcWc/htqlhrjFIjnHDM= -github.com/aws/aws-sdk-go-v2/service/sqs v1.42.21 h1:Oa0IhwDLVrcBHDlNo1aosG4CxO4HyvzDV5xUWqWcBc0= -github.com/aws/aws-sdk-go-v2/service/sqs v1.42.21/go.mod h1:t98Ssq+qtXKXl2SFtaSkuT6X42FSM//fnO6sfq5RqGM= -github.com/aws/aws-sdk-go-v2/service/ssm v1.67.8 h1:31Llf5VfrZ78YvYs7sWcS7L2m3waikzRc6q1nYenVS4= -github.com/aws/aws-sdk-go-v2/service/ssm v1.67.8/go.mod h1:/jgaDlU1UImoxTxhRNxXHvBAPqPZQ8oCjcPbbkR6kac= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= -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/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4= +github.com/aws/aws-sdk-go-v2/service/apigateway v1.38.5 h1:YQq9Nc7b1u4qIwUPQACr59mPCW3Gfb8QwFL7r4PxOP4= +github.com/aws/aws-sdk-go-v2/service/apigateway v1.38.5/go.mod h1:iRxNPQXn19AXRzweQQVRT153qLbmSzW6S6KKQYCYZ5U= +github.com/aws/aws-sdk-go-v2/service/autoscaling v1.64.1 h1:3eD5+Hg+h7XTwmix7vWf5oSIBp/1+KWync+JVsgfWsg= +github.com/aws/aws-sdk-go-v2/service/autoscaling v1.64.1/go.mod h1:c7Rb5WS2TW1nY+Mz60fPTdMAdkpZWCIzHz7HrNdKft8= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.1 h1:fwkGr0AyYMq/oxzBrNWVLcmSgSWVyGtFAanNs+ECRes= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.1/go.mod h1:PAegJVxp+CkgKZBZVEaTWBN2bHwH24FLl5sIIHYuzOU= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.55.0 h1:h3AU/3FXAFLwNFnbQCPSnak46FD69QwiD7OpB+afg3I= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.55.0/go.mod h1:SRVEOVD920otumvM08MTqzhQ916eYiDNGpHPB1dqxr8= +github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.12 h1:jTjCUj61HLa3rk/GGnoLjv5iElFDlmdCoXyqAPRxpy4= +github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.12/go.mod h1:TY3yvOssvgQqFskskRdAMQx+waaGKzoFX5HMXtOiRG8= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.0 h1:n5BubZVgbYyweQmdqMT+HMhH07wCxmMyBAQy/VhinoU= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.0/go.mod h1:IFMlDGLL3eM098XqgRk27wateJOnrzp7zz93Wh/F9qk= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.293.0 h1:dgdIaG/GCiXMo16HAdFwpjt9Vn34bD2WVH5SiZdwzUc= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.293.0/go.mod h1:2dMnUs1QzlGzsm46i9oBHAxVHQp7b6qF7PljWcgVEVE= +github.com/aws/aws-sdk-go-v2/service/ecs v1.73.0 h1:bZAxMktXWPmeWhB6I14LsJE2e+t6uLASV80xZdqqXlk= +github.com/aws/aws-sdk-go-v2/service/ecs v1.73.0/go.mod h1:DdtkqcURi9GM8f9HVLzJLTvS0h0k1qYg39vKQFmeR/k= +github.com/aws/aws-sdk-go-v2/service/efs v1.41.11 h1:tCCyWJmkqYJbdfS4Dm3Pyg07b1kp1wCcTgY6Q+FPvU0= +github.com/aws/aws-sdk-go-v2/service/efs v1.41.11/go.mod h1:sVhXa89shXJ36cMmBJPiPi8+s5NCO6gnnlKjjoGrL6s= +github.com/aws/aws-sdk-go-v2/service/eks v1.80.1 h1:Aivj88+23MYkW/B507eqsnLHTMmj4A/Us2AxKz+PDkM= +github.com/aws/aws-sdk-go-v2/service/eks v1.80.1/go.mod h1:p30UgulgoiPvwWGGfVeiaCbOzD1PTObBVYn6MmCPHVg= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.20 h1:kHQywC96ZviLmJJmgWKm6NTGX1BR3hEv52Gl82ik0i0= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.20/go.mod h1:bsLJBZhd8V2OqgNFn61nVh6PTluA4JZh+/DIneIntw4= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.7 h1:txeoy+BxL/Xef6Cl8zAq4ZewY7c+KnQ3gPSMSTTkTt4= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.7/go.mod h1:tv2v97S1V5kkp/1vneSYad5Cnrbo+4vfiNNAKCWNKIk= +github.com/aws/aws-sdk-go-v2/service/iam v1.53.3 h1:boKZv8dNdHznhAA68hb/dqFz5pxoWmRAOJr9LtscVCI= +github.com/aws/aws-sdk-go-v2/service/iam v1.53.3/go.mod h1:E0QHh3aEwxYb7xshjvxYDELiOda7KBYJ77e/TvGhpcM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.18 h1:J8H6iJPIb40gWCjAHfFCCergiy94TuJ5bFxaF+OGRcY= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.18/go.mod h1:59002AlnnGT2qznAiC0Hi+WhheaEWTiWyAeA9DQf0/w= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.1 h1:wb/PYYm3wlcqGzw7Ls4GD3X5+seDDoNdVYIB6I/V87E= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.1/go.mod h1:xvHowJ6J9CuaFE04S8fitWQXytf4sHz3DTPGhw9FtmU= +github.com/aws/aws-sdk-go-v2/service/lambda v1.88.1 h1:9WZiZ+1YXpvqvOi2CszopJJlzvv2h8cpxzPBy/rF+NA= +github.com/aws/aws-sdk-go-v2/service/lambda v1.88.1/go.mod h1:NFUHqj4J37VOyZvFHoMn4FjSBaFsPEHeTaBup0isZWM= +github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.4 h1:cWSEfMOtr75M7eecGNqqGS250cbddYSKmS6i35vm+EI= +github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.4/go.mod h1:YdidU5yM1JjqRMfLm8P0wsR3hdGsBUFffpnv66HCtvU= +github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.5 h1:35askM1RKc8/3JaO0Ad/wTzSgmw6/Z9KDX8/HQV/5XM= +github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.5/go.mod h1:cI5BdcbX2OU4pnvhgo/Wx0L2ODiq975nNpu7FkyWqW4= +github.com/aws/aws-sdk-go-v2/service/rds v1.116.1 h1:a5PMhM3lOcu2DKgvYGjhCDToKQnz9VEUo9iSc5+DsyA= +github.com/aws/aws-sdk-go-v2/service/rds v1.116.1/go.mod h1:bMaMwbVQ96bx42kDw/Ko+YiDyT/UCotPO+1RDp6lq7E= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.2 h1:zoD/SoiVQi8l8tuQn//VexrXS2yorg/+717JNA4Ble8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.2/go.mod h1:Ll1DCasPTBFtHK5t/U5WIwGIyRuY3xY+x8/LmqIlqpM= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0= +github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.2 h1:MJ6IIv3VdXESqoORpAgQJYSWLrY7G1AuT8XBQKWCUq8= +github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.2/go.mod h1:Qj7f4iKqd4n/UKcuWwlFhd1irk6S3H27r8QpfVItCZc= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= +github.com/aws/aws-sdk-go-v2/service/sns v1.39.12 h1:yVf0R6Mp8iXmy3/yCY97YyHB1VSkxlxK0ywh14tGuuk= +github.com/aws/aws-sdk-go-v2/service/sns v1.39.12/go.mod h1:9pHipxPwPZJcYm1TEU4gBzwcceAREvks2GDGJewm8Lo= +github.com/aws/aws-sdk-go-v2/service/sqs v1.42.22 h1:CVksqT2e8RFAixRTlDqu1nj174Vjb3VqG7wyZEAlYuA= +github.com/aws/aws-sdk-go-v2/service/sqs v1.42.22/go.mod h1:n3/KSi68g5s54U9J1FV4fRz8oK+7ML2RJK+mDu6gGS0= +github.com/aws/aws-sdk-go-v2/service/ssm v1.68.1 h1:kDgdZuYBWSsh3U/jZOXwcqfX6UsSzFcmtgKx7C0c5/E= +github.com/aws/aws-sdk-go-v2/service/ssm v1.68.1/go.mod h1:xyao5chroDlX/9q/rKBxRKZPv9NdG5Pm9W5zS+wQJ84= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= +github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= +github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= 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= @@ -450,8 +450,8 @@ github.com/exaring/otelpgx v0.10.0/go.mod h1:R5/M5LWsPPBZc1SrRE5e0DiU48bI78C1/GP 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/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 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= @@ -465,8 +465,8 @@ github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCK 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/getsentry/sentry-go v0.43.0 h1:XbXLpFicpo8HmBDaInk7dum18G9KSLcjZiyUKS+hLW4= +github.com/getsentry/sentry-go v0.43.0/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0= 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= @@ -485,6 +485,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 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-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -601,8 +603,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 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/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= 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= @@ -621,8 +623,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.9 h1:4l1t+7MovJVTyU2rTWUcI8tSsCSsMsMQC6U2Fculj7g= -github.com/harness/harness-go-sdk v0.7.9/go.mod h1:iEAGFfIm0MOFJxN6tqMQSPZiEO/Dz1joLDHrkEU3lps= +github.com/harness/harness-go-sdk v0.7.12 h1:CR/0NJljatoeDOnhrZ1otYj8cOB0ge+OD3hSU0Xat5Q= +github.com/harness/harness-go-sdk v0.7.12/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= @@ -664,10 +666,10 @@ github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5 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-framework v1.18.0 h1:Xy6OfqSTZfAAKXSlJ810lYvuQvYkOpSUoNMQ9l2L1RA= +github.com/hashicorp/terraform-plugin-framework v1.18.0/go.mod h1:eeFIf68PME+kenJeqSrIcpHhYQK0TOyv7ocKdN4Z35E= +github.com/hashicorp/terraform-plugin-go v0.30.0 h1:VmEiD0n/ewxbvV5VI/bYwNtlSEAXtHaZlSnyUUuQK6k= +github.com/hashicorp/terraform-plugin-go v0.30.0/go.mod h1:8d523ORAW8OHgA9e8JKg0ezL3XUO84H0A25o4NY/jRo= 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= @@ -722,8 +724,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.8 h1:BjiyVcJDwGrz01/9cvtX1ArNVvtybydGFDxoaU/6lsU= -github.com/kaptinlin/jsonrepair v0.2.8/go.mod h1:Lrh9CD/0CZyQDdLaZzE/rhNnjQmWezWwrAdJpqc1POg= +github.com/kaptinlin/jsonrepair v0.2.17 h1:fkEom1MBG98QeN7uaJpKBRA9st3bPdS32RK+im/IjCU= +github.com/kaptinlin/jsonrepair v0.2.17/go.mod h1:Hbq/F0frQBVClHW/oQXixaCPysZ6gdzpeUBZPpWQtAQ= 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= @@ -782,11 +784,10 @@ 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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -801,8 +802,8 @@ github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3v 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/micahhausler/aws-iam-policy v0.4.3 h1:TdJamVZxIyycdBR84JfI8+TVdPJ/3PEprWFbj6UdxaI= +github.com/micahhausler/aws-iam-policy v0.4.3/go.mod h1:H+yWljTu4XWJjNJJYgrPUai0AUTGNHc8pumkN57/foI= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= @@ -845,8 +846,8 @@ github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g= github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= github.com/nats-io/nats-server/v2 v2.12.4 h1:ZnT10v2LU2Xcoiy8ek9X6Se4YG8EuMfIfvAEuFVx1Ts= github.com/nats-io/nats-server/v2 v2.12.4/go.mod h1:5MCp/pqm5SEfsvVZ31ll1088ZTwEUdvRX1Hmh/mTTDg= -github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U= -github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE= +github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw= 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= @@ -871,8 +872,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.21.0 h1:3GpIR/W4q/v1uUOVuK3zYtQiF3DnRrZag/sxbtvEdtc= -github.com/openai/openai-go/v3 v3.21.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/openai/openai-go/v3 v3.24.0 h1:08x6GnYiB+AAejTo6yzPY8RkZMJQ8NpreiOyM5QfyYU= +github.com/openai/openai-go/v3 v3.24.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= @@ -962,16 +963,16 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94 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/river v0.31.0 h1:BERwce/WS4Guter0/A3GyTDP+1rxl6vFHyBQv+U/5tM= +github.com/riverqueue/river v0.31.0/go.mod h1:Aqbb/jBrFMvh6rbe6SDC6XVZnS0v1W+QQPjejRvyHzk= +github.com/riverqueue/river/riverdriver v0.31.0 h1:XwDa8DqkRxkqMqfdLOYTgSykiTHNSRcWG1LcCg/g0ys= +github.com/riverqueue/river/riverdriver v0.31.0/go.mod h1:Vl6XPbWtjqP+rqEa/HxcEeXeZL/KPCwqjRlqj+wWsq8= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.31.0 h1:Zii6/VNqasBuPvFIA98xgjz3MRy2EvMm6lMyh1RtWBw= +github.com/riverqueue/river/riverdriver/riverpgxv5 v0.31.0/go.mod h1:z859lpsOraO3IYWjY9w8RZec5I0BAcas9rjZkwxAijU= +github.com/riverqueue/river/rivershared v0.31.0 h1:KVEp+13jnK9YOlMUKnR0eUyJaK+P/APcheoSGMfZArA= +github.com/riverqueue/river/rivershared v0.31.0/go.mod h1:Wvf489bvAiZsJm7mln8YAPZbK7pVfuK7bYfsBt5Nzbw= +github.com/riverqueue/river/rivertype v0.31.0 h1:O6vaJ72SffgF1nxzCrDKd4M+eMZFRlJpycnOcUIGLD8= +github.com/riverqueue/river/rivertype v0.31.0/go.mod h1:D1Ad+EaZiaXbQbJcJcfeicXJMBKno0n6UcfKI5Q7DIQ= 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= @@ -1062,8 +1063,8 @@ 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/stripe/stripe-go/v84 v84.4.0 h1:JMQMqb+mhW6tns+eYA3G5SZiaoD2ULwN0lZ+kNjWAsY= +github.com/stripe/stripe-go/v84 v84.4.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= @@ -1162,8 +1163,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo 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 v1.18.0 h1:pJ8+HNI4gFoyRNqVE37wWbJWVw43BZczFo7KUoRczaA= +github.com/zclconf/go-cty v1.18.0/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= @@ -1286,8 +1287,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB 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/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= 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= @@ -1313,8 +1314,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.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= +golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= 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= @@ -1373,7 +1374,6 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc 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= @@ -1461,8 +1461,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.266.0 h1:hco+oNCf9y7DmLeAtHJi/uBAY7n/7XC9mZPxu1ROiyk= -google.golang.org/api v0.266.0/go.mod h1:Jzc0+ZfLnyvXma3UtaTl023TdhZu6OMBP9tJ+0EmFD0= +google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg= +google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE= 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= @@ -1540,18 +1540,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.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= -k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= +k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= 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/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= +k8s.io/apimachinery v0.35.2/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/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= +k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= +k8s.io/component-base v0.35.2 h1:btgR+qNrpWuRSuvWSnQYsZy88yf5gVwemvz0yw79pGc= +k8s.io/component-base v0.35.2/go.mod h1:B1iBJjooe6xIJYUucAxb26RwhAjzx0gHnqO9htWIX+0= 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= @@ -1580,14 +1580,14 @@ 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/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= +modernc.org/sqlite v1.46.1/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= +riverqueue.com/riverui v0.15.0 h1:7Xm/tqv63jZrGSv4X2u4zpAvbtXSs835Qk4RFonBDdk= +riverqueue.com/riverui v0.15.0/go.mod h1:J4fH8+zPe1cqmYWuMWVJdDdMmq1U2UPVofyOczGZNnw= 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= From 4283d88552c511b9dc60729571f2dbbe0d48b397 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:43:58 +0000 Subject: [PATCH 37/74] Eng 2872 create batchbatchapplication adapter (#4063) image --- > [!NOTE] > **Medium Risk** > Adds a new Azure discovery adapter/client for Batch Applications and wires it into adapter initialization, which can affect discovery coverage and linked-item graphs. Risk is moderate due to new paging/query logic and additional Azure SDK client initialization, though changes are isolated and covered by unit tests. > > **Overview** > Adds discovery support for Azure Batch Applications via a new `SearchableWrapper` (`batch-batch-application.go`) backed by a new `BatchApplicationsClient` interface and generated gomock (`batch-application-client.go`, `mock_batch_application_client.go`). The adapter supports `Get`, `Search`, and `SearchStream`, sets a composite unique attribute, and emits linked queries to the parent Batch Account and application packages. > > Wires the new adapter into `manual/adapters.go` by initializing an `armbatch.ApplicationClient` and registering `NewBatchBatchApplication` (including the metadata-only path), and updates `shared/utils.go` so ARM IDs for `azure-batch-batch-application` can be parsed into lookup parts. > > Also tightens nil handling when iterating Cosmos DB private endpoint connections (skip nil entries), and updates the adapter-creation skill doc to mandate using `new(...)` for pointer fields in test data to satisfy CI `go fix` behavior. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 477fa8c0e09640477e835129c8a6faa37a82c776. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 7158c837495627df4de1e197bb21658f02891e16 --- .../azure/clients/batch-application-client.go | 35 +++ sources/azure/manual/adapters.go | 10 + .../azure/manual/batch-batch-application.go | 232 ++++++++++++++++ .../manual/batch-batch-application_test.go | 254 ++++++++++++++++++ .../documentdb-private-endpoint-connection.go | 4 +- .../mocks/mock_batch_application_client.go | 72 +++++ sources/azure/shared/utils.go | 1 + 7 files changed, 606 insertions(+), 2 deletions(-) create mode 100644 sources/azure/clients/batch-application-client.go create mode 100644 sources/azure/manual/batch-batch-application.go create mode 100644 sources/azure/manual/batch-batch-application_test.go create mode 100644 sources/azure/shared/mocks/mock_batch_application_client.go diff --git a/sources/azure/clients/batch-application-client.go b/sources/azure/clients/batch-application-client.go new file mode 100644 index 00000000..8b3c5de3 --- /dev/null +++ b/sources/azure/clients/batch-application-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" +) + +//go:generate mockgen -destination=../shared/mocks/mock_batch_application_client.go -package=mocks -source=batch-application-client.go + +// BatchApplicationsPager is a type alias for the generic Pager interface with batch application response type. +type BatchApplicationsPager = Pager[armbatch.ApplicationClientListResponse] + +// BatchApplicationsClient is an interface for interacting with Azure Batch applications +type BatchApplicationsClient interface { + Get(ctx context.Context, resourceGroupName string, accountName string, applicationName string) (armbatch.ApplicationClientGetResponse, error) + List(ctx context.Context, resourceGroupName string, accountName string) BatchApplicationsPager +} + +type batchApplicationsClient struct { + client *armbatch.ApplicationClient +} + +func (c *batchApplicationsClient) Get(ctx context.Context, resourceGroupName string, accountName string, applicationName string) (armbatch.ApplicationClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, accountName, applicationName, nil) +} + +func (c *batchApplicationsClient) List(ctx context.Context, resourceGroupName string, accountName string) BatchApplicationsPager { + return c.client.NewListPager(resourceGroupName, accountName, nil) +} + +// NewBatchApplicationsClient creates a new BatchApplicationsClient from the Azure SDK client +func NewBatchApplicationsClient(client *armbatch.ApplicationClient) BatchApplicationsClient { + return &batchApplicationsClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 81c34f77..e1b34934 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -181,6 +181,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create batch accounts client: %w", err) } + batchApplicationClient, err := armbatch.NewApplicationClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create batch application client: %w", err) + } + virtualMachineScaleSetsClient, err := armcompute.NewVirtualMachineScaleSetsClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create virtual machine scale sets client: %w", err) @@ -455,6 +460,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewBatchAccountsClient(batchAccountsClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewBatchBatchApplication( + clients.NewBatchApplicationsClient(batchApplicationClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewComputeVirtualMachineScaleSet( clients.NewVirtualMachineScaleSetsClient(virtualMachineScaleSetsClient), resourceGroupScopes, @@ -614,6 +623,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewNetworkLoadBalancer(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkZone(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewBatchAccount(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewBatchBatchApplication(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeVirtualMachineScaleSet(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeAvailabilitySet(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeDisk(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/batch-batch-application.go b/sources/azure/manual/batch-batch-application.go new file mode 100644 index 00000000..6917520f --- /dev/null +++ b/sources/azure/manual/batch-batch-application.go @@ -0,0 +1,232 @@ +package manual + +import ( + "context" + "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" +) + +var BatchBatchApplicationLookupByName = shared.NewItemTypeLookup("name", azureshared.BatchBatchApplication) + +type batchBatchApplicationWrapper struct { + client clients.BatchApplicationsClient + *azureshared.MultiResourceGroupBase +} + +// NewBatchBatchApplication returns a SearchableWrapper for Azure Batch applications (child of Batch account). +func NewBatchBatchApplication(client clients.BatchApplicationsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &batchBatchApplicationWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.BatchBatchApplication, + ), + } +} + +func (b batchBatchApplicationWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: accountName and applicationName", + Scope: scope, + ItemType: b.Type(), + } + } + accountName := queryParts[0] + applicationName := queryParts[1] + + rgScope, err := b.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, b.Type()) + } + resp, err := b.client.Get(ctx, rgScope.ResourceGroup, accountName, applicationName) + if err != nil { + return nil, azureshared.QueryError(err, scope, b.Type()) + } + + return b.azureApplicationToSDPItem(&resp.Application, accountName, applicationName, scope) +} + +func (b batchBatchApplicationWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + BatchAccountLookupByName, + BatchBatchApplicationLookupByName, + } +} + +func (b batchBatchApplicationWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: accountName", + Scope: scope, + ItemType: b.Type(), + } + } + accountName := queryParts[0] + + rgScope, err := b.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, b.Type()) + } + pager := b.client.List(ctx, rgScope.ResourceGroup, accountName) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, b.Type()) + } + + for _, app := range page.Value { + if app == nil || app.Name == nil { + continue + } + item, sdpErr := b.azureApplicationToSDPItem(app, accountName, *app.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (b batchBatchApplicationWrapper) 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: accountName"), scope, b.Type())) + return + } + accountName := queryParts[0] + + rgScope, err := b.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, b.Type())) + return + } + pager := b.client.List(ctx, rgScope.ResourceGroup, accountName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, b.Type())) + return + } + for _, app := range page.Value { + if app == nil || app.Name == nil { + continue + } + item, sdpErr := b.azureApplicationToSDPItem(app, accountName, *app.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (b batchBatchApplicationWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + BatchAccountLookupByName, + }, + } +} + +func (b batchBatchApplicationWrapper) azureApplicationToSDPItem(app *armbatch.Application, accountName, applicationName, scope string) (*sdp.Item, *sdp.QueryError) { + if app.Name == nil { + return nil, azureshared.QueryError(errors.New("application name is nil"), scope, b.Type()) + } + attributes, err := shared.ToAttributesWithExclude(app, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, b.Type()) + } + + if err := attributes.Set("uniqueAttr", shared.CompositeLookupKey(accountName, applicationName)); err != nil { + return nil, azureshared.QueryError(err, scope, b.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.BatchBatchApplication.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(app.Tags), + LinkedItemQueries: []*sdp.LinkedItemQuery{}, + } + + // Link to parent Batch Account + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.BatchBatchAccount.String(), + Method: sdp.QueryMethod_GET, + Query: accountName, + Scope: scope, + }, + }) + + // Link to Application Packages (child resource under this application) + // Packages are listed under /batchAccounts/{account}/applications/{app}/versions + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.BatchBatchApplicationPackage.String(), + Method: sdp.QueryMethod_SEARCH, + Query: shared.CompositeLookupKey(accountName, applicationName), + Scope: scope, + }, + }) + + // Link to default version application package when set (GET to specific child resource) + if app.Properties != nil && app.Properties.DefaultVersion != nil && *app.Properties.DefaultVersion != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.BatchBatchApplicationPackage.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(accountName, applicationName, *app.Properties.DefaultVersion), + Scope: scope, + }, + }) + } + + return sdpItem, nil +} + +func (b batchBatchApplicationWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.BatchBatchAccount: true, + azureshared.BatchBatchApplicationPackage: true, + } +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/batch_application +func (b batchBatchApplicationWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_batch_application.id", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/compute +func (b batchBatchApplicationWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Batch/batchAccounts/applications/read", + } +} + +func (b batchBatchApplicationWrapper) PredefinedRole() string { + return "Azure Batch Account Reader" +} diff --git a/sources/azure/manual/batch-batch-application_test.go b/sources/azure/manual/batch-batch-application_test.go new file mode 100644 index 00000000..d5c20fd3 --- /dev/null +++ b/sources/azure/manual/batch-batch-application_test.go @@ -0,0 +1,254 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "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" +) + +// mockBatchApplicationsPager is a mock implementation of BatchApplicationsPager. +type mockBatchApplicationsPager struct { + pages []armbatch.ApplicationClientListResponse + index int +} + +func (m *mockBatchApplicationsPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockBatchApplicationsPager) NextPage(ctx context.Context) (armbatch.ApplicationClientListResponse, error) { + if m.index >= len(m.pages) { + return armbatch.ApplicationClientListResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +// errorBatchApplicationsPager is a mock pager that always returns an error. +type errorBatchApplicationsPager struct{} + +func (e *errorBatchApplicationsPager) More() bool { + return true +} + +func (e *errorBatchApplicationsPager) NextPage(ctx context.Context) (armbatch.ApplicationClientListResponse, error) { + return armbatch.ApplicationClientListResponse{}, errors.New("pager error") +} + +// testBatchApplicationsClient wraps the mock and injects a pager from List(). +type testBatchApplicationsClient struct { + *mocks.MockBatchApplicationsClient + pager clients.BatchApplicationsPager +} + +func (t *testBatchApplicationsClient) List(ctx context.Context, resourceGroupName, accountName string) clients.BatchApplicationsPager { + if t.pager != nil { + return t.pager + } + return t.MockBatchApplicationsClient.List(ctx, resourceGroupName, accountName) +} + +func createAzureBatchApplication(name string) *armbatch.Application { + allowUpdates := true + return &armbatch.Application{ + ID: new("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Batch/batchAccounts/acc/applications/" + name), + Name: new(name), + Type: new("Microsoft.Batch/batchAccounts/applications"), + Properties: &armbatch.ApplicationProperties{ + DisplayName: new("Test application " + name), + AllowUpdates: &allowUpdates, + }, + Tags: map[string]*string{"env": new("test")}, + } +} + +func TestBatchBatchApplication(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + accountName := "test-batch-account" + applicationName := "test-app" + + t.Run("Get", func(t *testing.T) { + app := createAzureBatchApplication(applicationName) + + mockClient := mocks.NewMockBatchApplicationsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, accountName, applicationName).Return( + armbatch.ApplicationClientGetResponse{ + Application: *app, + }, nil) + + wrapper := manual.NewBatchBatchApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(accountName, applicationName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.BatchBatchApplication.String() { + t.Errorf("Expected type %s, got %s", azureshared.BatchBatchApplication.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUnique := shared.CompositeLookupKey(accountName, applicationName) + if sdpItem.UniqueAttributeValue() != expectedUnique { + t.Errorf("Expected unique attribute value %s, got %s", expectedUnique, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != scope { + t.Errorf("Expected scope %s, got %s", scope, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected valid item, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.BatchBatchAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: accountName, ExpectedScope: scope}, + {ExpectedType: azureshared.BatchBatchApplicationPackage.String(), ExpectedMethod: sdp.QueryMethod_SEARCH, ExpectedQuery: shared.CompositeLookupKey(accountName, applicationName), ExpectedScope: scope}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockBatchApplicationsClient(ctrl) + wrapper := manual.NewBatchBatchApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, accountName, true) + if qErr == nil { + t.Error("Expected error when Get with insufficient query parts, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("application not found") + mockClient := mocks.NewMockBatchApplicationsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, accountName, "nonexistent").Return( + armbatch.ApplicationClientGetResponse{}, expectedErr) + + wrapper := manual.NewBatchBatchApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(accountName, "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) { + app1 := createAzureBatchApplication("app-1") + app2 := createAzureBatchApplication("app-2") + + mockClient := mocks.NewMockBatchApplicationsClient(ctrl) + pages := []armbatch.ApplicationClientListResponse{ + { + ListApplicationsResult: armbatch.ListApplicationsResult{ + Value: []*armbatch.Application{app1, app2}, + }, + }, + } + mockPager := &mockBatchApplicationsPager{pages: pages} + testClient := &testBatchApplicationsClient{ + MockBatchApplicationsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewBatchBatchApplication(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, accountName, 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.NewMockBatchApplicationsClient(ctrl) + wrapper := manual.NewBatchBatchApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Search(ctx, scope) + if qErr == nil { + t.Error("Expected error when Search with no query parts, but got nil") + } + }) + + t.Run("Search_PagerError", func(t *testing.T) { + mockClient := mocks.NewMockBatchApplicationsClient(ctrl) + errorPager := &errorBatchApplicationsPager{} + testClient := &testBatchApplicationsClient{ + MockBatchApplicationsClient: mockClient, + pager: errorPager, + } + + wrapper := manual.NewBatchBatchApplication(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Search(ctx, scope, accountName) + if qErr == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := mocks.NewMockBatchApplicationsClient(ctrl) + wrapper := manual.NewBatchBatchApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + links := wrapper.PotentialLinks() + if !links[azureshared.BatchBatchAccount] { + t.Error("PotentialLinks() should include BatchBatchAccount") + } + if !links[azureshared.BatchBatchApplicationPackage] { + t.Error("PotentialLinks() should include BatchBatchApplicationPackage") + } + }) + + t.Run("ImplementsSearchableAdapter", func(t *testing.T) { + mockClient := mocks.NewMockBatchApplicationsClient(ctrl) + wrapper := manual.NewBatchBatchApplication(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/documentdb-private-endpoint-connection.go b/sources/azure/manual/documentdb-private-endpoint-connection.go index 7030ceb7..8a2d5efa 100644 --- a/sources/azure/manual/documentdb-private-endpoint-connection.go +++ b/sources/azure/manual/documentdb-private-endpoint-connection.go @@ -95,7 +95,7 @@ func (s documentDBPrivateEndpointConnectionWrapper) Search(ctx context.Context, } for _, conn := range page.Value { - if conn.Name == nil { + if conn == nil || conn.Name == nil { continue } @@ -130,7 +130,7 @@ func (s documentDBPrivateEndpointConnectionWrapper) SearchStream(ctx context.Con return } for _, conn := range page.Value { - if conn.Name == nil { + if conn == nil || conn.Name == nil { continue } item, sdpErr := s.azurePrivateEndpointConnectionToSDPItem(conn, accountName, *conn.Name, scope) diff --git a/sources/azure/shared/mocks/mock_batch_application_client.go b/sources/azure/shared/mocks/mock_batch_application_client.go new file mode 100644 index 00000000..9ef5f92a --- /dev/null +++ b/sources/azure/shared/mocks/mock_batch_application_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: batch-application-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_batch_application_client.go -package=mocks -source=batch-application-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armbatch "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockBatchApplicationsClient is a mock of BatchApplicationsClient interface. +type MockBatchApplicationsClient struct { + ctrl *gomock.Controller + recorder *MockBatchApplicationsClientMockRecorder + isgomock struct{} +} + +// MockBatchApplicationsClientMockRecorder is the mock recorder for MockBatchApplicationsClient. +type MockBatchApplicationsClientMockRecorder struct { + mock *MockBatchApplicationsClient +} + +// NewMockBatchApplicationsClient creates a new mock instance. +func NewMockBatchApplicationsClient(ctrl *gomock.Controller) *MockBatchApplicationsClient { + mock := &MockBatchApplicationsClient{ctrl: ctrl} + mock.recorder = &MockBatchApplicationsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBatchApplicationsClient) EXPECT() *MockBatchApplicationsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockBatchApplicationsClient) Get(ctx context.Context, resourceGroupName, accountName, applicationName string) (armbatch.ApplicationClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, accountName, applicationName) + ret0, _ := ret[0].(armbatch.ApplicationClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockBatchApplicationsClientMockRecorder) Get(ctx, resourceGroupName, accountName, applicationName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockBatchApplicationsClient)(nil).Get), ctx, resourceGroupName, accountName, applicationName) +} + +// List mocks base method. +func (m *MockBatchApplicationsClient) List(ctx context.Context, resourceGroupName, accountName string) clients.BatchApplicationsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, resourceGroupName, accountName) + ret0, _ := ret[0].(clients.BatchApplicationsPager) + return ret0 +} + +// List indicates an expected call of List. +func (mr *MockBatchApplicationsClientMockRecorder) List(ctx, resourceGroupName, accountName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockBatchApplicationsClient)(nil).List), ctx, resourceGroupName, accountName) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index faba83bf..a69e6ac0 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -43,6 +43,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-network-virtual-network-peering": {"virtualNetworks", "virtualNetworkPeerings"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/virtualNetworkPeerings/{peeringName}", "azure-network-route": {"routeTables", "routes"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/routeTables/{routeTableName}/routes/{routeName}", "azure-network-security-rule": {"networkSecurityGroups", "securityRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/networkSecurityGroups/{nsgName}/securityRules/{ruleName}", + "azure-batch-batch-application": {"batchAccounts", "applications"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Batch/batchAccounts/{accountName}/applications/{applicationName}", } if keys, ok := pathKeysMap[resourceType]; ok { From b33b871a27b724d338c06588b38530f7675b24bd Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Tue, 3 Mar 2026 09:07:41 +0100 Subject: [PATCH 38/74] =?UTF-8?q?feat(sdp):=20add=20mappedItemRef=20to=20I?= =?UTF-8?q?temDiff=20for=20LLM-mapped=20blast=20radius=20=E2=80=A6=20(#405?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### VIDEO https://github.com/user-attachments/assets/76f355f0-7072-471a-81d8-c8afc6e57bc7 ### NEW image ### OLD image feat(sdp): link LLM-mapped items in blast radius graph (ENG-2837) When the LLM maps a custom Terraform resource to a live infrastructure item of a different type, the two nodes appeared disconnected in the blast radius graph. Bridge them with a new mappedItemRef field on ItemDiff. Proto: - Add optional Reference mappedItemRef = 6 to ItemDiff Backend: - Populate mappedItemRef in processUnmappedItem() after successful LLM matching, with nil-safe access via GetItem() - Add resolveLLMMappedReferences() to upgrade wildcard-scope refs into canonical gateway-verified references - Call resolveLLMMappedReferences() from CalculateMappedResources() - Update Area51 blast radius worker to run CalculateMappedResources first, reload persisted diffs, then calculate blast radius Frontend: - Create synthetic edges in BlastRadiusLoader connecting plan item nodes to their LLM-mapped live item nodes via mappedItemRef Tests: - E2E: mappedItemRef survives DB round-trip and blast radius snapshot - Unit: mock LLM provider verifies MappedItemRef population, empty scope wildcard fallback, nil on no match - Unit: resolveLLMMappedReferences skips items without refs, preserves original ref on gateway failure --- > [!NOTE] > **Medium Risk** > Adds a new proto field and threads it through LLM mapping + blast radius calculation, including a new gateway websocket resolution step that could affect blast-radius job behavior/performance if the gateway is slow/unreachable (with fallback behavior). Change is additive/optional but touches critical change-analysis pipeline and frontend graph rendering. > > **Overview** > Adds optional `mappedItemRef` to `changes.ItemDiff` (Go + TS generated outputs updated) to represent the live infra item an LLM matched when it differs from the plan item. > > Backend now populates `ItemDiff.mappedItemRef` on successful LLM matching, then attempts to *resolve* that reference via the gateway to a canonical graph node reference (real scope), and runs `CalculateMappedResources` before blast radius (reloading persisted diffs) so blast radius uses the enriched data. > > Frontend blast radius graph now creates synthetic edges from `diff.item` to `diff.mappedItemRef` so LLM-mapped plan nodes visibly connect to their matched live nodes; tests were added/expanded to cover DB round-trip and gateway-resolution behavior. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 383393463875349b3f80cf1c54cad2264ffb0aa8. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: f540f6f358e634b45091f38204e7627da7a3c2e8 --- go/sdp-go/changes.pb.go | 256 +++++++++++++++++++++------------------- 1 file changed, 137 insertions(+), 119 deletions(-) diff --git a/go/sdp-go/changes.pb.go b/go/sdp-go/changes.pb.go index 9a4130f2..51259dff 100644 --- a/go/sdp-go/changes.pb.go +++ b/go/sdp-go/changes.pb.go @@ -3645,8 +3645,15 @@ type ItemDiff struct { 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 + // Reference to the live infrastructure item this diff was mapped to via + // LLM mapping. Only set when the mapped item differs from the plan item + // (i.e., the plan resource type has no static mapping and the LLM found + // a matching live item of a different type). The frontend uses this to + // draw a synthetic edge in the blast radius graph connecting the plan + // item node to the mapped live item node. + MappedItemRef *Reference `protobuf:"bytes,6,opt,name=mappedItemRef,proto3,oneof" json:"mappedItemRef,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ItemDiff) Reset() { @@ -3714,6 +3721,13 @@ func (x *ItemDiff) GetModificationSummary() string { return "" } +func (x *ItemDiff) GetMappedItemRef() *Reference { + if x != nil { + return x.MappedItemRef + } + return nil +} + 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"` @@ -6736,15 +6750,18 @@ const file_changes_proto_rawDesc = "" + "\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" + + "\vhealthAfter\x18\x05 \x01(\x0e2\a.HealthR\vhealthAfter\"\xa0\x02\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" + + "\x13modificationSummary\x18\x05 \x01(\tR\x13modificationSummary\x125\n" + + "\rmappedItemRef\x18\x06 \x01(\v2\n" + + ".ReferenceH\x01R\rmappedItemRef\x88\x01\x01B\a\n" + + "\x05_itemB\x10\n" + + "\x0e_mappedItemRef\"\x9f\x01\n" + "\fEnrichedTags\x12?\n" + "\btagValue\x18\x12 \x03(\v2#.changes.EnrichedTags.TagValueEntryR\btagValue\x1aN\n" + "\rTagValueEntry\x12\x10\n" + @@ -7264,120 +7281,121 @@ var file_changes_proto_depIdxs = []int32{ 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 + 121, // 67: changes.ItemDiff.mappedItemRef:type_name -> Reference + 107, // 68: changes.EnrichedTags.tagValue:type_name -> changes.EnrichedTags.TagValueEntry + 65, // 69: changes.TagValue.userTagValue:type_name -> changes.UserTagValue + 66, // 70: changes.TagValue.autoTagValue:type_name -> changes.AutoTagValue + 6, // 71: changes.Label.type:type_name -> changes.LabelType + 7, // 72: changes.ChangeSummary.status:type_name -> changes.ChangeStatus + 111, // 73: changes.ChangeSummary.createdAt:type_name -> google.protobuf.Timestamp + 108, // 74: changes.ChangeSummary.tags:type_name -> changes.ChangeSummary.TagsEntry + 63, // 75: changes.ChangeSummary.enrichedTags:type_name -> changes.EnrichedTags + 67, // 76: changes.ChangeSummary.labels:type_name -> changes.Label + 72, // 77: changes.ChangeSummary.githubChangeInfo:type_name -> changes.GithubChangeInfo + 70, // 78: changes.Change.metadata:type_name -> changes.ChangeMetadata + 71, // 79: changes.Change.properties:type_name -> changes.ChangeProperties + 111, // 80: changes.ChangeMetadata.createdAt:type_name -> google.protobuf.Timestamp + 111, // 81: changes.ChangeMetadata.updatedAt:type_name -> google.protobuf.Timestamp + 7, // 82: changes.ChangeMetadata.status:type_name -> changes.ChangeStatus + 109, // 83: changes.ChangeMetadata.UnknownHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 109, // 84: changes.ChangeMetadata.OkHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 109, // 85: changes.ChangeMetadata.WarningHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 109, // 86: changes.ChangeMetadata.ErrorHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 109, // 87: changes.ChangeMetadata.PendingHealthChange:type_name -> changes.ChangeMetadata.HealthChange + 72, // 88: changes.ChangeMetadata.githubChangeInfo:type_name -> changes.GithubChangeInfo + 104, // 89: changes.ChangeMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus + 62, // 90: changes.ChangeProperties.plannedChanges:type_name -> changes.ItemDiff + 110, // 91: changes.ChangeProperties.tags:type_name -> changes.ChangeProperties.TagsEntry + 63, // 92: changes.ChangeProperties.enrichedTags:type_name -> changes.EnrichedTags + 67, // 93: changes.ChangeProperties.labels:type_name -> changes.Label + 69, // 94: changes.ListChangesResponse.changes:type_name -> changes.Change + 7, // 95: changes.ListChangesByStatusRequest.status:type_name -> changes.ChangeStatus + 69, // 96: changes.ListChangesByStatusResponse.changes:type_name -> changes.Change + 71, // 97: changes.CreateChangeRequest.properties:type_name -> changes.ChangeProperties + 69, // 98: changes.CreateChangeResponse.change:type_name -> changes.Change + 5, // 99: changes.GetChangeSummaryRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat + 10, // 100: changes.GetChangeSummaryRequest.riskSeverityFilter:type_name -> changes.Risk.Severity + 5, // 101: changes.GetChangeSignalsRequest.changeOutputFormat:type_name -> changes.ChangeOutputFormat + 69, // 102: changes.GetChangeResponse.change:type_name -> changes.Change + 104, // 103: changes.ChangeRiskMetadata.changeAnalysisStatus:type_name -> changes.ChangeAnalysisStatus + 103, // 104: changes.ChangeRiskMetadata.risks:type_name -> changes.Risk + 87, // 105: changes.GetChangeRisksResponse.changeRiskMetadata:type_name -> changes.ChangeRiskMetadata + 71, // 106: changes.UpdateChangeRequest.properties:type_name -> changes.ChangeProperties + 69, // 107: changes.UpdateChangeResponse.change:type_name -> changes.Change + 69, // 108: changes.ListChangesBySnapshotUUIDResponse.changes:type_name -> changes.Change + 8, // 109: changes.StartChangeResponse.state:type_name -> changes.StartChangeResponse.State + 9, // 110: changes.EndChangeResponse.state:type_name -> changes.EndChangeResponse.State + 10, // 111: changes.Risk.severity:type_name -> changes.Risk.Severity + 121, // 112: changes.Risk.relatedItems:type_name -> Reference + 11, // 113: changes.ChangeAnalysisStatus.status:type_name -> changes.ChangeAnalysisStatus.Status + 64, // 114: changes.EnrichedTags.TagValueEntry.value:type_name -> changes.TagValue + 73, // 115: changes.ChangesService.ListChanges:input_type -> changes.ListChangesRequest + 75, // 116: changes.ChangesService.ListChangesByStatus:input_type -> changes.ListChangesByStatusRequest + 77, // 117: changes.ChangesService.CreateChange:input_type -> changes.CreateChangeRequest + 79, // 118: changes.ChangesService.GetChange:input_type -> changes.GetChangeRequest + 80, // 119: changes.ChangesService.GetChangeByTicketLink:input_type -> changes.GetChangeByTicketLinkRequest + 81, // 120: changes.ChangesService.GetChangeSummary:input_type -> changes.GetChangeSummaryRequest + 34, // 121: changes.ChangesService.GetChangeTimelineV2:input_type -> changes.GetChangeTimelineV2Request + 86, // 122: changes.ChangesService.GetChangeRisks:input_type -> changes.GetChangeRisksRequest + 89, // 123: changes.ChangesService.UpdateChange:input_type -> changes.UpdateChangeRequest + 91, // 124: changes.ChangesService.DeleteChange:input_type -> changes.DeleteChangeRequest + 92, // 125: changes.ChangesService.ListChangesBySnapshotUUID:input_type -> changes.ListChangesBySnapshotUUIDRequest + 95, // 126: changes.ChangesService.RefreshState:input_type -> changes.RefreshStateRequest + 97, // 127: changes.ChangesService.StartChange:input_type -> changes.StartChangeRequest + 99, // 128: changes.ChangesService.EndChange:input_type -> changes.EndChangeRequest + 97, // 129: changes.ChangesService.StartChangeSimple:input_type -> changes.StartChangeRequest + 99, // 130: changes.ChangesService.EndChangeSimple:input_type -> changes.EndChangeRequest + 56, // 131: changes.ChangesService.ListHomeChanges:input_type -> changes.ListHomeChangesRequest + 54, // 132: changes.ChangesService.StartChangeAnalysis:input_type -> changes.StartChangeAnalysisRequest + 51, // 133: changes.ChangesService.ListChangingItemsSummary:input_type -> changes.ListChangingItemsSummaryRequest + 49, // 134: changes.ChangesService.GetDiff:input_type -> changes.GetDiffRequest + 59, // 135: changes.ChangesService.PopulateChangeFilters:input_type -> changes.PopulateChangeFiltersRequest + 105, // 136: changes.ChangesService.GenerateRiskFix:input_type -> changes.GenerateRiskFixRequest + 31, // 137: changes.ChangesService.GetHypothesesDetails:input_type -> changes.GetHypothesesDetailsRequest + 83, // 138: changes.ChangesService.GetChangeSignals:input_type -> changes.GetChangeSignalsRequest + 15, // 139: changes.LabelService.ListLabelRules:input_type -> changes.ListLabelRulesRequest + 17, // 140: changes.LabelService.CreateLabelRule:input_type -> changes.CreateLabelRuleRequest + 19, // 141: changes.LabelService.GetLabelRule:input_type -> changes.GetLabelRuleRequest + 21, // 142: changes.LabelService.UpdateLabelRule:input_type -> changes.UpdateLabelRuleRequest + 23, // 143: changes.LabelService.DeleteLabelRule:input_type -> changes.DeleteLabelRuleRequest + 25, // 144: changes.LabelService.TestLabelRule:input_type -> changes.TestLabelRuleRequest + 27, // 145: changes.LabelService.ReapplyLabelRuleInTimeRange:input_type -> changes.ReapplyLabelRuleInTimeRangeRequest + 74, // 146: changes.ChangesService.ListChanges:output_type -> changes.ListChangesResponse + 76, // 147: changes.ChangesService.ListChangesByStatus:output_type -> changes.ListChangesByStatusResponse + 78, // 148: changes.ChangesService.CreateChange:output_type -> changes.CreateChangeResponse + 85, // 149: changes.ChangesService.GetChange:output_type -> changes.GetChangeResponse + 85, // 150: changes.ChangesService.GetChangeByTicketLink:output_type -> changes.GetChangeResponse + 82, // 151: changes.ChangesService.GetChangeSummary:output_type -> changes.GetChangeSummaryResponse + 35, // 152: changes.ChangesService.GetChangeTimelineV2:output_type -> changes.GetChangeTimelineV2Response + 88, // 153: changes.ChangesService.GetChangeRisks:output_type -> changes.GetChangeRisksResponse + 90, // 154: changes.ChangesService.UpdateChange:output_type -> changes.UpdateChangeResponse + 94, // 155: changes.ChangesService.DeleteChange:output_type -> changes.DeleteChangeResponse + 93, // 156: changes.ChangesService.ListChangesBySnapshotUUID:output_type -> changes.ListChangesBySnapshotUUIDResponse + 96, // 157: changes.ChangesService.RefreshState:output_type -> changes.RefreshStateResponse + 98, // 158: changes.ChangesService.StartChange:output_type -> changes.StartChangeResponse + 100, // 159: changes.ChangesService.EndChange:output_type -> changes.EndChangeResponse + 101, // 160: changes.ChangesService.StartChangeSimple:output_type -> changes.StartChangeSimpleResponse + 102, // 161: changes.ChangesService.EndChangeSimple:output_type -> changes.EndChangeSimpleResponse + 58, // 162: changes.ChangesService.ListHomeChanges:output_type -> changes.ListHomeChangesResponse + 55, // 163: changes.ChangesService.StartChangeAnalysis:output_type -> changes.StartChangeAnalysisResponse + 52, // 164: changes.ChangesService.ListChangingItemsSummary:output_type -> changes.ListChangingItemsSummaryResponse + 50, // 165: changes.ChangesService.GetDiff:output_type -> changes.GetDiffResponse + 60, // 166: changes.ChangesService.PopulateChangeFilters:output_type -> changes.PopulateChangeFiltersResponse + 106, // 167: changes.ChangesService.GenerateRiskFix:output_type -> changes.GenerateRiskFixResponse + 32, // 168: changes.ChangesService.GetHypothesesDetails:output_type -> changes.GetHypothesesDetailsResponse + 84, // 169: changes.ChangesService.GetChangeSignals:output_type -> changes.GetChangeSignalsResponse + 16, // 170: changes.LabelService.ListLabelRules:output_type -> changes.ListLabelRulesResponse + 18, // 171: changes.LabelService.CreateLabelRule:output_type -> changes.CreateLabelRuleResponse + 20, // 172: changes.LabelService.GetLabelRule:output_type -> changes.GetLabelRuleResponse + 22, // 173: changes.LabelService.UpdateLabelRule:output_type -> changes.UpdateLabelRuleResponse + 24, // 174: changes.LabelService.DeleteLabelRule:output_type -> changes.DeleteLabelRuleResponse + 26, // 175: changes.LabelService.TestLabelRule:output_type -> changes.TestLabelRuleResponse + 28, // 176: changes.LabelService.ReapplyLabelRuleInTimeRange:output_type -> changes.ReapplyLabelRuleInTimeRangeResponse + 146, // [146:177] is the sub-list for method output_type + 115, // [115:146] is the sub-list for method input_type + 115, // [115:115] is the sub-list for extension type_name + 115, // [115:115] is the sub-list for extension extendee + 0, // [0:115] is the sub-list for field type_name } func init() { file_changes_proto_init() } From 24cf2ce033e6faf959e802639189deaf69c0f606 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Tue, 3 Mar 2026 09:15:14 +0100 Subject: [PATCH 39/74] Eng 2871 create batchbatchpool adapter (#4068) > [!NOTE] > **Medium Risk** > Adds a new Azure Batch Pools adapter and wires it into the Azure adapter set, which will change discovery output and increase Azure API calls for Batch accounts. Risk is mainly around correctness of the new resource/link parsing and paging logic rather than existing behavior changes. > > **Overview** > Adds first-class discovery support for Azure Batch pools via a new `BatchPoolsClient` wrapper (plus gomock) and a new `NewBatchBatchPool` searchable adapter. > > The adapter supports `Get` and `Search` (list-by-account) and enriches pool items with many new `LinkedItemQueries` (batch account, subnet/public IPs, managed identities, app packages/certs, storage mounts, image references, container registries, and StartTask URIs), including basic IP/DNS deduping and a mount-source host parser. > > Wires the new adapter into `manual/adapters.go` (including placeholder registration) and extends `shared.GetResourceIDPathKeys` with `azure-batch-batch-pool`; adds unit tests covering Get/Search, error paths, and link metadata. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8e8f37d20ce97961bced6b22e7966d579b61bd26. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: fdfe401c1cbbff7e9640290298d285bdb0ab210b --- sources/azure/clients/batch-pool-client.go | 35 + sources/azure/manual/adapters.go | 10 + sources/azure/manual/batch-batch-pool.go | 727 ++++++++++++++++++ sources/azure/manual/batch-batch-pool_test.go | 275 +++++++ .../shared/mocks/mock_batch_pool_client.go | 72 ++ sources/azure/shared/utils.go | 1 + 6 files changed, 1120 insertions(+) create mode 100644 sources/azure/clients/batch-pool-client.go create mode 100644 sources/azure/manual/batch-batch-pool.go create mode 100644 sources/azure/manual/batch-batch-pool_test.go create mode 100644 sources/azure/shared/mocks/mock_batch_pool_client.go diff --git a/sources/azure/clients/batch-pool-client.go b/sources/azure/clients/batch-pool-client.go new file mode 100644 index 00000000..22788075 --- /dev/null +++ b/sources/azure/clients/batch-pool-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" +) + +//go:generate mockgen -destination=../shared/mocks/mock_batch_pool_client.go -package=mocks -source=batch-pool-client.go + +// BatchPoolsPager is a type alias for the generic Pager interface with batch pool response type. +type BatchPoolsPager = Pager[armbatch.PoolClientListByBatchAccountResponse] + +// BatchPoolsClient is an interface for interacting with Azure Batch pools (child of Batch account). +type BatchPoolsClient interface { + Get(ctx context.Context, resourceGroupName string, accountName string, poolName string) (armbatch.PoolClientGetResponse, error) + ListByBatchAccount(ctx context.Context, resourceGroupName string, accountName string) BatchPoolsPager +} + +type batchPoolsClient struct { + client *armbatch.PoolClient +} + +func (c *batchPoolsClient) Get(ctx context.Context, resourceGroupName string, accountName string, poolName string) (armbatch.PoolClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, accountName, poolName, nil) +} + +func (c *batchPoolsClient) ListByBatchAccount(ctx context.Context, resourceGroupName string, accountName string) BatchPoolsPager { + return c.client.NewListByBatchAccountPager(resourceGroupName, accountName, nil) +} + +// NewBatchPoolsClient creates a new BatchPoolsClient from the Azure SDK client. +func NewBatchPoolsClient(client *armbatch.PoolClient) BatchPoolsClient { + return &batchPoolsClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index e1b34934..88c82173 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -186,6 +186,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create batch application client: %w", err) } + batchPoolClient, err := armbatch.NewPoolClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create batch pool client: %w", err) + } + virtualMachineScaleSetsClient, err := armcompute.NewVirtualMachineScaleSetsClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create virtual machine scale sets client: %w", err) @@ -464,6 +469,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewBatchApplicationsClient(batchApplicationClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewBatchBatchPool( + clients.NewBatchPoolsClient(batchPoolClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewComputeVirtualMachineScaleSet( clients.NewVirtualMachineScaleSetsClient(virtualMachineScaleSetsClient), resourceGroupScopes, @@ -624,6 +633,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewNetworkZone(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewBatchAccount(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewBatchBatchApplication(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewBatchBatchPool(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeVirtualMachineScaleSet(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeAvailabilitySet(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeDisk(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/batch-batch-pool.go b/sources/azure/manual/batch-batch-pool.go new file mode 100644 index 00000000..9575116a --- /dev/null +++ b/sources/azure/manual/batch-batch-pool.go @@ -0,0 +1,727 @@ +package manual + +import ( + "context" + "errors" + "fmt" + "net" + "net/url" + "strings" + + "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" +) + +var BatchBatchPoolLookupByName = shared.NewItemTypeLookup("name", azureshared.BatchBatchPool) + +type batchBatchPoolWrapper struct { + client clients.BatchPoolsClient + *azureshared.MultiResourceGroupBase +} + +// NewBatchBatchPool returns a SearchableWrapper for Azure Batch pools (child of Batch account). +func NewBatchBatchPool(client clients.BatchPoolsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &batchBatchPoolWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.BatchBatchPool, + ), + } +} + +func (b batchBatchPoolWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: accountName and poolName", + Scope: scope, + ItemType: b.Type(), + } + } + accountName := queryParts[0] + poolName := queryParts[1] + + rgScope, err := b.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, b.Type()) + } + resp, err := b.client.Get(ctx, rgScope.ResourceGroup, accountName, poolName) + if err != nil { + return nil, azureshared.QueryError(err, scope, b.Type()) + } + + return b.azurePoolToSDPItem(&resp.Pool, accountName, poolName, scope) +} + +func (b batchBatchPoolWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + BatchAccountLookupByName, + BatchBatchPoolLookupByName, + } +} + +func (b batchBatchPoolWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: accountName", + Scope: scope, + ItemType: b.Type(), + } + } + accountName := queryParts[0] + + rgScope, err := b.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, b.Type()) + } + pager := b.client.ListByBatchAccount(ctx, rgScope.ResourceGroup, accountName) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, b.Type()) + } + + for _, pool := range page.Value { + if pool == nil || pool.Name == nil { + continue + } + item, sdpErr := b.azurePoolToSDPItem(pool, accountName, *pool.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (b batchBatchPoolWrapper) 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: accountName"), scope, b.Type())) + return + } + accountName := queryParts[0] + + rgScope, err := b.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, b.Type())) + return + } + pager := b.client.ListByBatchAccount(ctx, rgScope.ResourceGroup, accountName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, b.Type())) + return + } + for _, pool := range page.Value { + if pool == nil || pool.Name == nil { + continue + } + item, sdpErr := b.azurePoolToSDPItem(pool, accountName, *pool.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (b batchBatchPoolWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + BatchAccountLookupByName, + }, + } +} + +func (b batchBatchPoolWrapper) azurePoolToSDPItem(pool *armbatch.Pool, accountName, poolName, scope string) (*sdp.Item, *sdp.QueryError) { + if pool.Name == nil { + return nil, azureshared.QueryError(errors.New("pool name is nil"), scope, b.Type()) + } + attributes, err := shared.ToAttributesWithExclude(pool, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, b.Type()) + } + + if err := attributes.Set("uniqueAttr", shared.CompositeLookupKey(accountName, poolName)); err != nil { + return nil, azureshared.QueryError(err, scope, b.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.BatchBatchPool.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(pool.Tags), + LinkedItemQueries: []*sdp.LinkedItemQuery{}, + } + + // Link to parent Batch Account + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.BatchBatchAccount.String(), + Method: sdp.QueryMethod_GET, + Query: accountName, + Scope: scope, + }, + }) + + // Link to public IPs when NetworkConfiguration.PublicIPAddressConfiguration.IPAddressIDs is set + if pool.Properties != nil && pool.Properties.NetworkConfiguration != nil && pool.Properties.NetworkConfiguration.PublicIPAddressConfiguration != nil { + for _, ipIDPtr := range pool.Properties.NetworkConfiguration.PublicIPAddressConfiguration.IPAddressIDs { + if ipIDPtr == nil || *ipIDPtr == "" { + continue + } + ipName := azureshared.ExtractResourceName(*ipIDPtr) + if ipName == "" { + continue + } + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(*ipIDPtr); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPublicIPAddress.String(), + Method: sdp.QueryMethod_GET, + Query: ipName, + Scope: linkedScope, + }, + }) + } + } + + // Link to Subnet when NetworkConfiguration.SubnetID is set + if pool.Properties != nil && pool.Properties.NetworkConfiguration != nil && pool.Properties.NetworkConfiguration.SubnetID != nil { + subnetID := *pool.Properties.NetworkConfiguration.SubnetID + scopeParams := azureshared.ExtractPathParamsFromResourceID(subnetID, []string{"subscriptions", "resourceGroups"}) + subnetParams := azureshared.ExtractPathParamsFromResourceID(subnetID, []string{"virtualNetworks", "subnets"}) + if len(scopeParams) >= 2 && len(subnetParams) >= 2 { + subnetScope := fmt.Sprintf("%s.%s", scopeParams[0], scopeParams[1]) + vnetName := subnetParams[0] + subnetName := subnetParams[1] + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkSubnet.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(vnetName, subnetName), + Scope: subnetScope, + }, + }) + } + } + + // Link to user-assigned managed identities from Identity.UserAssignedIdentities map keys (resource IDs) + if pool.Identity != nil && pool.Identity.UserAssignedIdentities != nil { + for identityResourceID := range pool.Identity.UserAssignedIdentities { + if identityResourceID == "" { + continue + } + identityName := azureshared.ExtractResourceName(identityResourceID) + if identityName == "" { + continue + } + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(identityResourceID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ManagedIdentityUserAssignedIdentity.String(), + Method: sdp.QueryMethod_GET, + Query: identityName, + Scope: linkedScope, + }, + }) + } + } + + // Link to application packages referenced by the pool (Properties.ApplicationPackages) + // ID can be .../batchAccounts/{account}/applications/{app}/versions/{version} (specific version) + // or .../applications/{app} (default version); when default, use pkgRef.Version as fallback. + if pool.Properties != nil && pool.Properties.ApplicationPackages != nil { + for _, pkgRef := range pool.Properties.ApplicationPackages { + if pkgRef == nil || pkgRef.ID == nil || *pkgRef.ID == "" { + continue + } + var pkgAccountName, appName, version string + params := azureshared.ExtractPathParamsFromResourceID(*pkgRef.ID, []string{"batchAccounts", "applications", "versions"}) + if len(params) >= 3 { + pkgAccountName, appName, version = params[0], params[1], params[2] + } else { + paramsApp := azureshared.ExtractPathParamsFromResourceID(*pkgRef.ID, []string{"batchAccounts", "applications"}) + if len(paramsApp) < 2 { + continue + } + pkgAccountName, appName = paramsApp[0], paramsApp[1] + if pkgRef.Version != nil && *pkgRef.Version != "" { + version = *pkgRef.Version + } else { + // Default version reference with no Version field: cannot form GET (adapter needs account|app|version) + continue + } + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.BatchBatchApplicationPackage.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(pkgAccountName, appName, version), + Scope: scope, + }, + }) + } + } + + // Link to certificates referenced by the pool (Properties.Certificates) + // ID format: .../batchAccounts/{account}/certificates/{thumbprint} + if pool.Properties != nil && pool.Properties.Certificates != nil { + for _, certRef := range pool.Properties.Certificates { + if certRef == nil || certRef.ID == nil || *certRef.ID == "" { + continue + } + params := azureshared.ExtractPathParamsFromResourceID(*certRef.ID, []string{"batchAccounts", "certificates"}) + if len(params) < 2 { + continue + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.BatchBatchCertificate.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(params[0], params[1]), + Scope: scope, + }, + }) + } + } + + // Link to storage accounts and IP/DNS from MountConfiguration + seenIPs := make(map[string]struct{}) + seenDNS := make(map[string]struct{}) + if pool.Properties != nil && pool.Properties.MountConfiguration != nil { + for _, mount := range pool.Properties.MountConfiguration { + if mount == nil { + continue + } + if mount.AzureBlobFileSystemConfiguration != nil { + blobCfg := mount.AzureBlobFileSystemConfiguration + if blobCfg.AccountName != nil && *blobCfg.AccountName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageAccount.String(), + Method: sdp.QueryMethod_GET, + Query: *blobCfg.AccountName, + Scope: scope, + }, + }) + } + if blobCfg.AccountName != nil && *blobCfg.AccountName != "" && blobCfg.ContainerName != nil && *blobCfg.ContainerName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageBlobContainer.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(*blobCfg.AccountName, *blobCfg.ContainerName), + Scope: scope, + }, + }) + } + if blobCfg.IdentityReference != nil && blobCfg.IdentityReference.ResourceID != nil && *blobCfg.IdentityReference.ResourceID != "" { + identityResourceID := *blobCfg.IdentityReference.ResourceID + identityName := azureshared.ExtractResourceName(identityResourceID) + if identityName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(identityResourceID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ManagedIdentityUserAssignedIdentity.String(), + Method: sdp.QueryMethod_GET, + Query: identityName, + Scope: linkedScope, + }, + }) + } + } + } + if mount.AzureFileShareConfiguration != nil { + if mount.AzureFileShareConfiguration.AccountName != nil && *mount.AzureFileShareConfiguration.AccountName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.StorageAccount.String(), + Method: sdp.QueryMethod_GET, + Query: *mount.AzureFileShareConfiguration.AccountName, + Scope: scope, + }, + }) + } + if mount.AzureFileShareConfiguration.AzureFileURL != nil && *mount.AzureFileShareConfiguration.AzureFileURL != "" { + AppendURILinks(&sdpItem.LinkedItemQueries, *mount.AzureFileShareConfiguration.AzureFileURL, seenDNS, seenIPs) + } + } + if mount.CifsMountConfiguration != nil && mount.CifsMountConfiguration.Source != nil && *mount.CifsMountConfiguration.Source != "" { + appendMountSourceHostLink(&sdpItem.LinkedItemQueries, *mount.CifsMountConfiguration.Source, seenIPs, seenDNS) + } + if mount.NfsMountConfiguration != nil && mount.NfsMountConfiguration.Source != nil && *mount.NfsMountConfiguration.Source != "" { + appendMountSourceHostLink(&sdpItem.LinkedItemQueries, *mount.NfsMountConfiguration.Source, seenIPs, seenDNS) + } + } + } + + // Link to image reference from DeploymentConfiguration.VirtualMachineConfiguration.ImageReference + // (custom image, shared gallery image, or community gallery image) + if pool.Properties != nil && pool.Properties.DeploymentConfiguration != nil && + pool.Properties.DeploymentConfiguration.VirtualMachineConfiguration != nil { + imageRef := pool.Properties.DeploymentConfiguration.VirtualMachineConfiguration.ImageReference + if imageRef != nil { + // ImageReference.ID: custom image or gallery image version path + if imageRef.ID != nil && *imageRef.ID != "" { + imageID := *imageRef.ID + if strings.Contains(imageID, "/galleries/") && strings.Contains(imageID, "/images/") && strings.Contains(imageID, "/versions/") { + params := azureshared.ExtractPathParamsFromResourceID(imageID, []string{"galleries", "images", "versions"}) + if len(params) == 3 { + galleryName, imageName, versionName := params[0], params[1], params[2] + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(imageID); extractedScope != "" { + linkScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeSharedGalleryImage.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(galleryName, imageName, versionName), + Scope: linkScope, + }, + }) + } + } else if strings.Contains(imageID, "/images/") { + imageName := azureshared.ExtractResourceName(imageID) + if imageName != "" { + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(imageID); extractedScope != "" { + linkScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeImage.String(), + Method: sdp.QueryMethod_GET, + Query: imageName, + Scope: linkScope, + }, + }) + } + } + } + // SharedGalleryImageID (path: .../sharedGalleries/{name}/images/{name}/versions/{name}) + if imageRef.SharedGalleryImageID != nil && *imageRef.SharedGalleryImageID != "" { + sharedGalleryImageID := *imageRef.SharedGalleryImageID + parts := azureshared.ExtractPathParamsFromResourceID(sharedGalleryImageID, []string{"sharedGalleries", "images", "versions"}) + if len(parts) >= 3 { + galleryName, imageName, version := parts[0], parts[1], parts[2] + linkScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(sharedGalleryImageID); extractedScope != "" { + linkScope = extractedScope + } + 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: linkScope, + }, + }) + } + } + // CommunityGalleryImageID + if imageRef.CommunityGalleryImageID != nil && *imageRef.CommunityGalleryImageID != "" { + communityGalleryImageID := *imageRef.CommunityGalleryImageID + parts := azureshared.ExtractPathParamsFromResourceID(communityGalleryImageID, []string{"CommunityGalleries", "Images", "Versions"}) + if len(parts) >= 3 { + communityGalleryName, imageName, version := parts[0], parts[1], parts[2] + 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: scope, + }, + }) + } + } + } + // Container registries (RegistryServer → DNS link; IdentityReference → managed identity link) + vmConfig := pool.Properties.DeploymentConfiguration.VirtualMachineConfiguration + if vmConfig.ContainerConfiguration != nil && vmConfig.ContainerConfiguration.ContainerRegistries != nil { + for _, reg := range vmConfig.ContainerConfiguration.ContainerRegistries { + if reg == nil { + continue + } + if reg.RegistryServer != nil && *reg.RegistryServer != "" { + host := strings.TrimSpace(*reg.RegistryServer) + if host != "" { + if net.ParseIP(host) != nil { + if _, seen := seenIPs[host]; !seen { + seenIPs[host] = struct{}{} + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: host, + Scope: "global", + }, + }) + } + } else { + if _, seen := seenDNS[host]; !seen { + seenDNS[host] = struct{}{} + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: host, + Scope: "global", + }, + }) + } + } + } + } + if reg.IdentityReference != nil && reg.IdentityReference.ResourceID != nil && *reg.IdentityReference.ResourceID != "" { + identityResourceID := *reg.IdentityReference.ResourceID + identityName := azureshared.ExtractResourceName(identityResourceID) + if identityName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(identityResourceID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ManagedIdentityUserAssignedIdentity.String(), + Method: sdp.QueryMethod_GET, + Query: identityName, + Scope: linkedScope, + }, + }) + } + } + } + } + } + + // StartTask: ResourceFiles (HTTPUrl, StorageContainerURL → URI links; IdentityReference → managed identity), ContainerSettings.Registry (RegistryServer → DNS; IdentityReference → managed identity) + if pool.Properties != nil && pool.Properties.StartTask != nil { + startTask := pool.Properties.StartTask + if startTask.ResourceFiles != nil { + for _, rf := range startTask.ResourceFiles { + if rf == nil { + continue + } + if rf.HTTPURL != nil && *rf.HTTPURL != "" { + AppendURILinks(&sdpItem.LinkedItemQueries, *rf.HTTPURL, seenDNS, seenIPs) + } + if rf.StorageContainerURL != nil && *rf.StorageContainerURL != "" { + AppendURILinks(&sdpItem.LinkedItemQueries, *rf.StorageContainerURL, seenDNS, seenIPs) + } + if rf.IdentityReference != nil && rf.IdentityReference.ResourceID != nil && *rf.IdentityReference.ResourceID != "" { + identityResourceID := *rf.IdentityReference.ResourceID + identityName := azureshared.ExtractResourceName(identityResourceID) + if identityName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(identityResourceID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ManagedIdentityUserAssignedIdentity.String(), + Method: sdp.QueryMethod_GET, + Query: identityName, + Scope: linkedScope, + }, + }) + } + } + } + } + if startTask.ContainerSettings != nil && startTask.ContainerSettings.Registry != nil { + reg := startTask.ContainerSettings.Registry + if reg.RegistryServer != nil && *reg.RegistryServer != "" { + host := strings.TrimSpace(*reg.RegistryServer) + if host != "" { + if net.ParseIP(host) != nil { + if _, seen := seenIPs[host]; !seen { + seenIPs[host] = struct{}{} + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: host, + Scope: "global", + }, + }) + } + } else { + if _, seen := seenDNS[host]; !seen { + seenDNS[host] = struct{}{} + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: host, + Scope: "global", + }, + }) + } + } + } + } + if reg.IdentityReference != nil && reg.IdentityReference.ResourceID != nil && *reg.IdentityReference.ResourceID != "" { + identityResourceID := *reg.IdentityReference.ResourceID + identityName := azureshared.ExtractResourceName(identityResourceID) + if identityName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(identityResourceID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ManagedIdentityUserAssignedIdentity.String(), + Method: sdp.QueryMethod_GET, + Query: identityName, + Scope: linkedScope, + }, + }) + } + } + } + } + + // Map provisioning state to health + if pool.Properties != nil && pool.Properties.ProvisioningState != nil { + switch *pool.Properties.ProvisioningState { + case armbatch.PoolProvisioningStateSucceeded: + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case armbatch.PoolProvisioningStateDeleting: + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + return sdpItem, nil +} + +func (b batchBatchPoolWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.BatchBatchAccount: true, + azureshared.NetworkSubnet: true, + azureshared.NetworkPublicIPAddress: true, + azureshared.ManagedIdentityUserAssignedIdentity: true, + azureshared.BatchBatchApplicationPackage: true, + azureshared.BatchBatchCertificate: true, + azureshared.StorageAccount: true, + azureshared.StorageBlobContainer: true, + azureshared.ComputeImage: true, + azureshared.ComputeSharedGalleryImage: true, + azureshared.ComputeCommunityGalleryImage: true, + stdlib.NetworkIP: true, + stdlib.NetworkDNS: true, + stdlib.NetworkHTTP: true, + } +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/batch_pool +func (b batchBatchPoolWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_batch_pool.id", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/permissions/compute +func (b batchBatchPoolWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Batch/batchAccounts/pools/read", + } +} + +func (b batchBatchPoolWrapper) PredefinedRole() string { + return "Azure Batch Account Reader" +} + +// appendMountSourceHostLink extracts a host from a CIFS or NFS mount source (e.g. "\\server\share", "nfs://host/path", or "192.168.1.1") and appends a NetworkIP or NetworkDNS linked query with deduplication. +func appendMountSourceHostLink(queries *[]*sdp.LinkedItemQuery, source string, seenIPs, seenDNS map[string]struct{}) { + if source == "" { + return + } + var host string + if after, ok := strings.CutPrefix(source, "\\\\"); ok { + // UNC path: \\server\share + rest := after + if before, _, ok := strings.Cut(rest, "\\"); ok { + host = before + } else { + host = rest + } + } else if strings.Contains(source, "://") { + u, err := url.Parse(source) + if err != nil || u.Host == "" { + return + } + host = u.Hostname() + } else { + // NFS format: host:/path (e.g. 192.168.1.1:/vol1) — split on ":/" so host has no trailing colon + if before, _, ok0 := strings.Cut(source, ":/"); ok0 { + host = before + } else if idx := strings.IndexAny(source, "/\\"); idx >= 0 { + host = source[:idx] + } else { + host = source + } + } + host = strings.TrimSpace(host) + if host == "" { + return + } + if net.ParseIP(host) != nil { + if _, seen := seenIPs[host]; !seen { + seenIPs[host] = struct{}{} + *queries = append(*queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: host, + Scope: "global", + }, + }) + } + } else { + if _, seen := seenDNS[host]; !seen { + seenDNS[host] = struct{}{} + *queries = append(*queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: host, + Scope: "global", + }, + }) + } + } +} diff --git a/sources/azure/manual/batch-batch-pool_test.go b/sources/azure/manual/batch-batch-pool_test.go new file mode 100644 index 00000000..f1e1f421 --- /dev/null +++ b/sources/azure/manual/batch-batch-pool_test.go @@ -0,0 +1,275 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "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" +) + +type mockBatchPoolsPager struct { + pages []armbatch.PoolClientListByBatchAccountResponse + index int +} + +func (m *mockBatchPoolsPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockBatchPoolsPager) NextPage(ctx context.Context) (armbatch.PoolClientListByBatchAccountResponse, error) { + if m.index >= len(m.pages) { + return armbatch.PoolClientListByBatchAccountResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type errorBatchPoolsPager struct{} + +func (e *errorBatchPoolsPager) More() bool { + return true +} + +func (e *errorBatchPoolsPager) NextPage(ctx context.Context) (armbatch.PoolClientListByBatchAccountResponse, error) { + return armbatch.PoolClientListByBatchAccountResponse{}, errors.New("pager error") +} + +type testBatchPoolsClient struct { + *mocks.MockBatchPoolsClient + pager clients.BatchPoolsPager +} + +func (t *testBatchPoolsClient) ListByBatchAccount(ctx context.Context, resourceGroupName, accountName string) clients.BatchPoolsPager { + if t.pager != nil { + return t.pager + } + return t.MockBatchPoolsClient.ListByBatchAccount(ctx, resourceGroupName, accountName) +} + +func createAzureBatchPool(name string) *armbatch.Pool { + state := armbatch.PoolProvisioningStateSucceeded + return &armbatch.Pool{ + ID: new("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Batch/batchAccounts/acc/pools/" + name), + Name: new(name), + Type: new("Microsoft.Batch/batchAccounts/pools"), + Properties: &armbatch.PoolProperties{ + VMSize: new("Standard_D2s_v3"), + ProvisioningState: &state, + }, + Tags: map[string]*string{"env": new("test")}, + } +} + +func TestBatchBatchPool(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + accountName := "test-batch-account" + poolName := "test-pool" + + t.Run("Get", func(t *testing.T) { + pool := createAzureBatchPool(poolName) + + mockClient := mocks.NewMockBatchPoolsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, accountName, poolName).Return( + armbatch.PoolClientGetResponse{ + Pool: *pool, + }, nil) + + wrapper := manual.NewBatchBatchPool(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(accountName, poolName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.BatchBatchPool.String() { + t.Errorf("Expected type %s, got %s", azureshared.BatchBatchPool.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUnique := shared.CompositeLookupKey(accountName, poolName) + if sdpItem.UniqueAttributeValue() != expectedUnique { + t.Errorf("Expected unique attribute value %s, got %s", expectedUnique, sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != scope { + t.Errorf("Expected scope %s, got %s", scope, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected valid item, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.BatchBatchAccount.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: accountName, ExpectedScope: scope}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockBatchPoolsClient(ctrl) + wrapper := manual.NewBatchBatchPool(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, accountName, true) + if qErr == nil { + t.Error("Expected error when Get with insufficient query parts, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("pool not found") + mockClient := mocks.NewMockBatchPoolsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, accountName, "nonexistent").Return( + armbatch.PoolClientGetResponse{}, expectedErr) + + wrapper := manual.NewBatchBatchPool(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(accountName, "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) { + pool1 := createAzureBatchPool("pool-1") + pool2 := createAzureBatchPool("pool-2") + + mockClient := mocks.NewMockBatchPoolsClient(ctrl) + pages := []armbatch.PoolClientListByBatchAccountResponse{ + { + ListPoolsResult: armbatch.ListPoolsResult{ + Value: []*armbatch.Pool{pool1, pool2}, + }, + }, + } + mockPager := &mockBatchPoolsPager{pages: pages} + testClient := &testBatchPoolsClient{ + MockBatchPoolsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewBatchBatchPool(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, accountName, 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.NewMockBatchPoolsClient(ctrl) + wrapper := manual.NewBatchBatchPool(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Search(ctx, scope) + if qErr == nil { + t.Error("Expected error when Search with no query parts, but got nil") + } + }) + + t.Run("Search_PagerError", func(t *testing.T) { + mockClient := mocks.NewMockBatchPoolsClient(ctrl) + errorPager := &errorBatchPoolsPager{} + testClient := &testBatchPoolsClient{ + MockBatchPoolsClient: mockClient, + pager: errorPager, + } + + wrapper := manual.NewBatchBatchPool(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + _, qErr := wrapper.Search(ctx, scope, accountName) + if qErr == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := mocks.NewMockBatchPoolsClient(ctrl) + wrapper := manual.NewBatchBatchPool(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + links := wrapper.PotentialLinks() + if !links[azureshared.BatchBatchAccount] { + t.Error("PotentialLinks() should include BatchBatchAccount") + } + if !links[azureshared.NetworkSubnet] { + t.Error("PotentialLinks() should include NetworkSubnet") + } + if !links[azureshared.ManagedIdentityUserAssignedIdentity] { + t.Error("PotentialLinks() should include ManagedIdentityUserAssignedIdentity") + } + if !links[azureshared.BatchBatchApplicationPackage] { + t.Error("PotentialLinks() should include BatchBatchApplicationPackage") + } + if !links[azureshared.BatchBatchCertificate] { + t.Error("PotentialLinks() should include BatchBatchCertificate") + } + if !links[azureshared.NetworkPublicIPAddress] { + t.Error("PotentialLinks() should include NetworkPublicIPAddress") + } + if !links[azureshared.StorageAccount] { + t.Error("PotentialLinks() should include StorageAccount") + } + if !links[stdlib.NetworkIP] { + t.Error("PotentialLinks() should include stdlib.NetworkIP") + } + if !links[stdlib.NetworkDNS] { + t.Error("PotentialLinks() should include stdlib.NetworkDNS") + } + if !links[stdlib.NetworkHTTP] { + t.Error("PotentialLinks() should include stdlib.NetworkHTTP") + } + }) + + t.Run("ImplementsSearchableAdapter", func(t *testing.T) { + mockClient := mocks.NewMockBatchPoolsClient(ctrl) + wrapper := manual.NewBatchBatchPool(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/shared/mocks/mock_batch_pool_client.go b/sources/azure/shared/mocks/mock_batch_pool_client.go new file mode 100644 index 00000000..d973e2de --- /dev/null +++ b/sources/azure/shared/mocks/mock_batch_pool_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: batch-pool-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_batch_pool_client.go -package=mocks -source=batch-pool-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armbatch "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockBatchPoolsClient is a mock of BatchPoolsClient interface. +type MockBatchPoolsClient struct { + ctrl *gomock.Controller + recorder *MockBatchPoolsClientMockRecorder + isgomock struct{} +} + +// MockBatchPoolsClientMockRecorder is the mock recorder for MockBatchPoolsClient. +type MockBatchPoolsClientMockRecorder struct { + mock *MockBatchPoolsClient +} + +// NewMockBatchPoolsClient creates a new mock instance. +func NewMockBatchPoolsClient(ctrl *gomock.Controller) *MockBatchPoolsClient { + mock := &MockBatchPoolsClient{ctrl: ctrl} + mock.recorder = &MockBatchPoolsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockBatchPoolsClient) EXPECT() *MockBatchPoolsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockBatchPoolsClient) Get(ctx context.Context, resourceGroupName, accountName, poolName string) (armbatch.PoolClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, accountName, poolName) + ret0, _ := ret[0].(armbatch.PoolClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockBatchPoolsClientMockRecorder) Get(ctx, resourceGroupName, accountName, poolName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockBatchPoolsClient)(nil).Get), ctx, resourceGroupName, accountName, poolName) +} + +// ListByBatchAccount mocks base method. +func (m *MockBatchPoolsClient) ListByBatchAccount(ctx context.Context, resourceGroupName, accountName string) clients.BatchPoolsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByBatchAccount", ctx, resourceGroupName, accountName) + ret0, _ := ret[0].(clients.BatchPoolsPager) + return ret0 +} + +// ListByBatchAccount indicates an expected call of ListByBatchAccount. +func (mr *MockBatchPoolsClientMockRecorder) ListByBatchAccount(ctx, resourceGroupName, accountName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByBatchAccount", reflect.TypeOf((*MockBatchPoolsClient)(nil).ListByBatchAccount), ctx, resourceGroupName, accountName) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index a69e6ac0..551d06e5 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -44,6 +44,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-network-route": {"routeTables", "routes"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/routeTables/{routeTableName}/routes/{routeName}", "azure-network-security-rule": {"networkSecurityGroups", "securityRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/networkSecurityGroups/{nsgName}/securityRules/{ruleName}", "azure-batch-batch-application": {"batchAccounts", "applications"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Batch/batchAccounts/{accountName}/applications/{applicationName}", + "azure-batch-batch-pool": {"batchAccounts", "pools"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Batch/batchAccounts/{accountName}/pools/{poolName}", } if keys, ok := pathKeysMap[resourceType]; ok { From 5d7526f7ca0a901727a84a5d9d433ef47b9d2d56 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:22:01 +0000 Subject: [PATCH 40/74] Eng 2874 create computededicatedhost adapter (#4080) image > [!NOTE] > **Medium Risk** > Adds a new Azure compute adapter and wires it into adapter initialization, which changes discovery coverage and introduces new Azure API calls (potentially affecting performance/quotas). Remaining changes are test/mocking refactors with low runtime risk. > > **Overview** > Adds a new `ComputeDedicatedHost` searchable adapter that can `Get` and list dedicated hosts by host group, emitting links to the parent `ComputeDedicatedHostGroup` and any deployed `ComputeVirtualMachine`s and deriving item health from provisioning state. > > Wires the new adapter into `manual/adapters.go` by creating an `armcompute.DedicatedHostsClient` and registering `NewComputeDedicatedHost(...)` (including the metadata/placeholder adapter path), and extends Azure resource ID path-key extraction to support `azure-compute-dedicated-host`. > > Refactors tests to use shared mocks (`azure/shared/mocks`) for gallery application/version/image clients, removes now-redundant generated mock files, and adds dedicated-host unit tests plus a generated `MockDedicatedHostsClient`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit a6f846fbf09f9f3b4a989639a92ba0ecfd7eeb83. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: ea304479021143514de2536081e37100580aa18b --- .../azure/clients/dedicated-hosts-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + .../azure/manual/compute-dedicated-host.go | 255 +++++++++++++ .../manual/compute-dedicated-host_test.go | 346 ++++++++++++++++++ ...ompute-gallery-application-version_test.go | 29 +- .../compute-gallery-application_test.go | 25 +- .../manual/compute-gallery-image_test.go | 33 +- ...allery_application_versions_client_test.go | 72 ---- .../mock_gallery_applications_client_test.go | 72 ---- .../manual/mock_gallery_images_client_test.go | 72 ---- .../mocks/mock_dedicated_hosts_client.go | 72 ++++ sources/azure/shared/utils.go | 1 + 12 files changed, 764 insertions(+), 258 deletions(-) create mode 100644 sources/azure/clients/dedicated-hosts-client.go create mode 100644 sources/azure/manual/compute-dedicated-host.go create mode 100644 sources/azure/manual/compute-dedicated-host_test.go delete mode 100644 sources/azure/manual/mock_gallery_application_versions_client_test.go delete mode 100644 sources/azure/manual/mock_gallery_applications_client_test.go delete mode 100644 sources/azure/manual/mock_gallery_images_client_test.go create mode 100644 sources/azure/shared/mocks/mock_dedicated_hosts_client.go diff --git a/sources/azure/clients/dedicated-hosts-client.go b/sources/azure/clients/dedicated-hosts-client.go new file mode 100644 index 00000000..e82bf6e9 --- /dev/null +++ b/sources/azure/clients/dedicated-hosts-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_dedicated_hosts_client.go -package=mocks -source=dedicated-hosts-client.go + +// DedicatedHostsPager is a type alias for the generic Pager interface with dedicated hosts list response type. +type DedicatedHostsPager = Pager[armcompute.DedicatedHostsClientListByHostGroupResponse] + +// DedicatedHostsClient is an interface for interacting with Azure dedicated hosts +type DedicatedHostsClient interface { + NewListByHostGroupPager(resourceGroupName string, hostGroupName string, options *armcompute.DedicatedHostsClientListByHostGroupOptions) DedicatedHostsPager + Get(ctx context.Context, resourceGroupName string, hostGroupName string, hostName string, options *armcompute.DedicatedHostsClientGetOptions) (armcompute.DedicatedHostsClientGetResponse, error) +} + +type dedicatedHostsClient struct { + client *armcompute.DedicatedHostsClient +} + +func (c *dedicatedHostsClient) NewListByHostGroupPager(resourceGroupName string, hostGroupName string, options *armcompute.DedicatedHostsClientListByHostGroupOptions) DedicatedHostsPager { + return c.client.NewListByHostGroupPager(resourceGroupName, hostGroupName, options) +} + +func (c *dedicatedHostsClient) Get(ctx context.Context, resourceGroupName string, hostGroupName string, hostName string, options *armcompute.DedicatedHostsClientGetOptions) (armcompute.DedicatedHostsClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, hostGroupName, hostName, options) +} + +// NewDedicatedHostsClient creates a new DedicatedHostsClient from the Azure SDK client +func NewDedicatedHostsClient(client *armcompute.DedicatedHostsClient) DedicatedHostsClient { + return &dedicatedHostsClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 88c82173..cfd91fc4 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -323,6 +323,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create dedicated host groups client: %w", err) } + dedicatedHostsClient, err := armcompute.NewDedicatedHostsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create dedicated hosts 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) @@ -561,6 +566,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewDedicatedHostGroupsClient(dedicatedHostGroupsClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewComputeDedicatedHost( + clients.NewDedicatedHostsClient(dedicatedHostsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewComputeCapacityReservationGroup( clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), resourceGroupScopes, @@ -655,6 +664,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(NewComputeDedicatedHost(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeCapacityReservationGroup(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeGalleryApplicationVersion(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeGalleryApplication(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/compute-dedicated-host.go b/sources/azure/manual/compute-dedicated-host.go new file mode 100644 index 00000000..2f27444a --- /dev/null +++ b/sources/azure/manual/compute-dedicated-host.go @@ -0,0 +1,255 @@ +package manual + +import ( + "context" + "errors" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +var ComputeDedicatedHostLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeDedicatedHost) + +type computeDedicatedHostWrapper struct { + client clients.DedicatedHostsClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeDedicatedHost(client clients.DedicatedHostsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &computeDedicatedHostWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeDedicatedHost, + ), + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/dedicated-hosts/get?view=rest-compute-2025-04-01&tabs=HTTP +func (c *computeDedicatedHostWrapper) 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: dedicated host group name and dedicated host name"), scope, c.Type()) + } + hostGroupName := queryParts[0] + if hostGroupName == "" { + return nil, azureshared.QueryError(errors.New("dedicated host group name cannot be empty"), scope, c.Type()) + } + hostName := queryParts[1] + if hostName == "" { + return nil, azureshared.QueryError(errors.New("dedicated host 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, hostGroupName, hostName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureDedicatedHostToSDPItem(&resp.DedicatedHost, hostGroupName, scope) +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/dedicated-hosts/list-by-host-group?view=rest-compute-2025-04-01&tabs=HTTP +func (c *computeDedicatedHostWrapper) 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: dedicated host group name"), scope, c.Type()) + } + hostGroupName := queryParts[0] + if hostGroupName == "" { + 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()) + } + pager := c.client.NewListByHostGroupPager(rgScope.ResourceGroup, hostGroupName, 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 _, host := range page.Value { + if host == nil || host.Name == nil { + continue + } + item, sdpErr := c.azureDedicatedHostToSDPItem(host, hostGroupName, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c *computeDedicatedHostWrapper) 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: dedicated host group name"), scope, c.Type())) + return + } + hostGroupName := queryParts[0] + if hostGroupName == "" { + stream.SendError(azureshared.QueryError(errors.New("dedicated host group 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.NewListByHostGroupPager(rgScope.ResourceGroup, hostGroupName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, host := range page.Value { + if host == nil || host.Name == nil { + continue + } + item, sdpErr := c.azureDedicatedHostToSDPItem(host, hostGroupName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (c *computeDedicatedHostWrapper) azureDedicatedHostToSDPItem(host *armcompute.DedicatedHost, hostGroupName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(host, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + if host.Name == nil { + return nil, azureshared.QueryError(errors.New("dedicated host name is nil"), scope, c.Type()) + } + hostName := *host.Name + if hostName == "" { + return nil, azureshared.QueryError(errors.New("dedicated host name cannot be empty"), scope, c.Type()) + } + if err := attributes.Set("uniqueAttr", shared.CompositeLookupKey(hostGroupName, hostName)); err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + + // Parent: dedicated host group + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeDedicatedHostGroup.String(), + Method: sdp.QueryMethod_GET, + Query: hostGroupName, + Scope: scope, + }, + }) + + // VMs deployed on this dedicated host + if host.Properties != nil && host.Properties.VirtualMachines != nil { + for _, vmRef := range host.Properties.VirtualMachines { + if vmRef == nil || vmRef.ID == nil || *vmRef.ID == "" { + continue + } + vmName := azureshared.ExtractResourceName(*vmRef.ID) + if vmName == "" { + continue + } + vmScope := scope + if linkScope := azureshared.ExtractScopeFromResourceID(*vmRef.ID); linkScope != "" { + vmScope = linkScope + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeVirtualMachine.String(), + Method: sdp.QueryMethod_GET, + Query: vmName, + Scope: vmScope, + }, + }) + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeDedicatedHost.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(host.Tags), + LinkedItemQueries: linkedItemQueries, + } + + // Health status from ProvisioningState + if host.Properties != nil && host.Properties.ProvisioningState != nil { + state := strings.ToLower(*host.Properties.ProvisioningState) + switch state { + 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() + } + } + + return sdpItem, nil +} + +func (c *computeDedicatedHostWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeDedicatedHostGroupLookupByName, + ComputeDedicatedHostLookupByName, + } +} + +func (c *computeDedicatedHostWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + ComputeDedicatedHostGroupLookupByName, + }, + } +} + +func (c *computeDedicatedHostWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.ComputeDedicatedHostGroup: true, + azureshared.ComputeVirtualMachine: true, + } +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/dedicated_host +func (c *computeDedicatedHostWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_dedicated_host.id", + }, + } +} + +func (c *computeDedicatedHostWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/hostGroups/hosts/read", + } +} + +func (c *computeDedicatedHostWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-dedicated-host_test.go b/sources/azure/manual/compute-dedicated-host_test.go new file mode 100644 index 00000000..b3f1204b --- /dev/null +++ b/sources/azure/manual/compute-dedicated-host_test.go @@ -0,0 +1,346 @@ +package manual + +import ( + "context" + "errors" + "testing" + + "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/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" +) + +func createAzureDedicatedHost(hostName, hostGroupName string) *armcompute.DedicatedHost { + return &armcompute.DedicatedHost{ + ID: new("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/hostGroups/" + hostGroupName + "/hosts/" + hostName), + Name: new(hostName), + Type: new("Microsoft.Compute/hostGroups/hosts"), + Location: new("eastus"), + Tags: map[string]*string{"env": new("test")}, + SKU: &armcompute.SKU{ + Name: new("DSv3-Type1"), + }, + Properties: &armcompute.DedicatedHostProperties{ + PlatformFaultDomain: new(int32(0)), + ProvisioningState: new("Succeeded"), + }, + } +} + +func createAzureDedicatedHostWithVMs(hostName, hostGroupName, subscriptionID, resourceGroup string, vmNames ...string) *armcompute.DedicatedHost { + vms := make([]*armcompute.SubResourceReadOnly, 0, len(vmNames)) + for _, vmName := range vmNames { + vms = append(vms, &armcompute.SubResourceReadOnly{ + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/" + vmName), + }) + } + return &armcompute.DedicatedHost{ + ID: new("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/hostGroups/" + hostGroupName + "/hosts/" + hostName), + Name: new(hostName), + Type: new("Microsoft.Compute/hostGroups/hosts"), + Location: new("eastus"), + Tags: map[string]*string{"env": new("test")}, + SKU: &armcompute.SKU{ + Name: new("DSv3-Type1"), + }, + Properties: &armcompute.DedicatedHostProperties{ + PlatformFaultDomain: new(int32(0)), + ProvisioningState: new("Succeeded"), + VirtualMachines: vms, + }, + } +} + +type mockDedicatedHostsPager struct { + items []*armcompute.DedicatedHost + index int +} + +func (m *mockDedicatedHostsPager) More() bool { + return m.index < len(m.items) +} + +func (m *mockDedicatedHostsPager) NextPage(ctx context.Context) (armcompute.DedicatedHostsClientListByHostGroupResponse, error) { + if m.index >= len(m.items) { + return armcompute.DedicatedHostsClientListByHostGroupResponse{ + DedicatedHostListResult: armcompute.DedicatedHostListResult{ + Value: []*armcompute.DedicatedHost{}, + }, + }, nil + } + item := m.items[m.index] + m.index++ + return armcompute.DedicatedHostsClientListByHostGroupResponse{ + DedicatedHostListResult: armcompute.DedicatedHostListResult{ + Value: []*armcompute.DedicatedHost{item}, + }, + }, nil +} + +type errorDedicatedHostsPager struct{} + +func (e *errorDedicatedHostsPager) More() bool { + return true +} + +func (e *errorDedicatedHostsPager) NextPage(ctx context.Context) (armcompute.DedicatedHostsClientListByHostGroupResponse, error) { + return armcompute.DedicatedHostsClientListByHostGroupResponse{}, errors.New("pager error") +} + +type testDedicatedHostsClient struct { + *mocks.MockDedicatedHostsClient + pager clients.DedicatedHostsPager +} + +func (t *testDedicatedHostsClient) NewListByHostGroupPager(resourceGroupName string, hostGroupName string, options *armcompute.DedicatedHostsClientListByHostGroupOptions) clients.DedicatedHostsPager { + if t.pager != nil { + return t.pager + } + return t.MockDedicatedHostsClient.NewListByHostGroupPager(resourceGroupName, hostGroupName, options) +} + +func TestComputeDedicatedHost(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + hostGroupName := "test-host-group" + hostName := "test-host" + + t.Run("Get", func(t *testing.T) { + host := createAzureDedicatedHost(hostName, hostGroupName) + + mockClient := mocks.NewMockDedicatedHostsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, hostGroupName, hostName, nil).Return( + armcompute.DedicatedHostsClientGetResponse{ + DedicatedHost: *host, + }, nil) + + wrapper := NewComputeDedicatedHost(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(hostGroupName, hostName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeDedicatedHost.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeDedicatedHost.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUnique := shared.CompositeLookupKey(hostGroupName, hostName) + 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.ComputeDedicatedHostGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: hostGroupName, ExpectedScope: scope}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_WithVMLinks", func(t *testing.T) { + host := createAzureDedicatedHostWithVMs(hostName, hostGroupName, subscriptionID, resourceGroup, "vm-1", "vm-2") + + mockClient := mocks.NewMockDedicatedHostsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, hostGroupName, hostName, nil).Return( + armcompute.DedicatedHostsClientGetResponse{ + DedicatedHost: *host, + }, nil) + + wrapper := NewComputeDedicatedHost(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(hostGroupName, hostName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeDedicatedHostGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: hostGroupName, ExpectedScope: scope}, + {ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "vm-1", ExpectedScope: scope}, + {ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "vm-2", ExpectedScope: scope}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostsClient(ctrl) + wrapper := NewComputeDedicatedHost(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, hostGroupName, true) + if qErr == nil { + t.Error("Expected error when Get with wrong number of query parts, but got nil") + } + }) + + t.Run("Get_EmptyHostGroupName", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostsClient(ctrl) + wrapper := NewComputeDedicatedHost(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey("", hostName) + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when host group name is empty, but got nil") + } + }) + + t.Run("Get_EmptyHostName", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostsClient(ctrl) + wrapper := NewComputeDedicatedHost(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(hostGroupName, "") + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when host name is empty, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("host not found") + mockClient := mocks.NewMockDedicatedHostsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, hostGroupName, "nonexistent", nil).Return( + armcompute.DedicatedHostsClientGetResponse{}, expectedErr) + + wrapper := NewComputeDedicatedHost(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(hostGroupName, "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) { + host1 := createAzureDedicatedHost("host-1", hostGroupName) + host2 := createAzureDedicatedHost("host-2", hostGroupName) + + mockClient := mocks.NewMockDedicatedHostsClient(ctrl) + pager := &mockDedicatedHostsPager{ + items: []*armcompute.DedicatedHost{host1, host2}, + } + testClient := &testDedicatedHostsClient{ + MockDedicatedHostsClient: mockClient, + pager: pager, + } + + wrapper := NewComputeDedicatedHost(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, hostGroupName, 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.NewMockDedicatedHostsClient(ctrl) + wrapper := NewComputeDedicatedHost(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, scope, hostGroupName, hostName) + if qErr == nil { + t.Error("Expected error when Search with wrong number of query parts, but got nil") + } + }) + + t.Run("Search_EmptyHostGroupName", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostsClient(ctrl) + wrapper := NewComputeDedicatedHost(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, scope, "") + if qErr == nil { + t.Error("Expected error when host group name is empty, but got nil") + } + }) + + t.Run("Search_PagerError", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostsClient(ctrl) + errorPager := &errorDedicatedHostsPager{} + testClient := &testDedicatedHostsClient{ + MockDedicatedHostsClient: mockClient, + pager: errorPager, + } + + wrapper := NewComputeDedicatedHost(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, hostGroupName, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := mocks.NewMockDedicatedHostsClient(ctrl) + wrapper := NewComputeDedicatedHost(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + links := wrapper.PotentialLinks() + expected := map[shared.ItemType]bool{ + azureshared.ComputeDedicatedHostGroup: true, + azureshared.ComputeVirtualMachine: 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.NewMockDedicatedHostsClient(ctrl) + wrapper := NewComputeDedicatedHost(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, ok := adapter.(discovery.SearchableAdapter) + if !ok { + t.Error("Adapter should implement SearchableAdapter interface") + } + }) +} diff --git a/sources/azure/manual/compute-gallery-application-version_test.go b/sources/azure/manual/compute-gallery-application-version_test.go index 37e262b1..3b580627 100644 --- a/sources/azure/manual/compute-gallery-application-version_test.go +++ b/sources/azure/manual/compute-gallery-application-version_test.go @@ -14,6 +14,7 @@ import ( "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/azure/shared/mocks" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -50,7 +51,7 @@ func (e *errorGalleryApplicationVersionsPager) NextPage(ctx context.Context) (ar // testGalleryApplicationVersionsClient wraps the mock and returns a pager from NewListByGalleryApplicationPager. type testGalleryApplicationVersionsClient struct { - *MockGalleryApplicationVersionsClient + *mocks.MockGalleryApplicationVersionsClient pager clients.GalleryApplicationVersionsPager } @@ -111,7 +112,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { t.Run("Get", func(t *testing.T) { version := createAzureGalleryApplicationVersion(galleryApplicationVersionName) - mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationVersionsClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( armcompute.GalleryApplicationVersionsClientGetResponse{ GalleryApplicationVersion: *version, @@ -159,7 +160,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { t.Run("GetWithLinkedResources", func(t *testing.T) { version := createAzureGalleryApplicationVersionWithLinks(galleryApplicationVersionName, subscriptionID, resourceGroup) - mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationVersionsClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( armcompute.GalleryApplicationVersionsClientGetResponse{ GalleryApplicationVersion: *version, @@ -191,7 +192,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { }) t.Run("Get_InvalidQueryParts", func(t *testing.T) { - mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationVersionsClient(ctrl) wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -203,7 +204,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { }) t.Run("Get_EmptyGalleryName", func(t *testing.T) { - mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationVersionsClient(ctrl) wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -216,7 +217,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { t.Run("Get_ClientError", func(t *testing.T) { expectedErr := errors.New("version not found") - mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationVersionsClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, "nonexistent", nil).Return( armcompute.GalleryApplicationVersionsClientGetResponse{}, expectedErr) @@ -235,7 +236,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { version := createAzureGalleryApplicationVersion(galleryApplicationVersionName) version.Properties.PublishingProfile.Source.MediaLink = new("https://example.com/artifacts/app.zip") - mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationVersionsClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( armcompute.GalleryApplicationVersionsClientGetResponse{ GalleryApplicationVersion: *version, @@ -287,7 +288,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { version := createAzureGalleryApplicationVersion(galleryApplicationVersionName) version.Properties.PublishingProfile.Source.MediaLink = new("https://192.168.1.10:8443/artifacts/app.zip") - mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationVersionsClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, galleryApplicationVersionName, nil).Return( armcompute.GalleryApplicationVersionsClientGetResponse{ GalleryApplicationVersion: *version, @@ -328,7 +329,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { v1 := createAzureGalleryApplicationVersion("1.0.0") v2 := createAzureGalleryApplicationVersion("1.0.1") - mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationVersionsClient(ctrl) pages := []armcompute.GalleryApplicationVersionsClientListByGalleryApplicationResponse{ { GalleryApplicationVersionList: armcompute.GalleryApplicationVersionList{ @@ -368,7 +369,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { }) t.Run("Search_InvalidQueryParts", func(t *testing.T) { - mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationVersionsClient(ctrl) wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -384,7 +385,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { }) t.Run("Search_EmptyGalleryName", func(t *testing.T) { - mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationVersionsClient(ctrl) wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) _, qErr := wrapper.Search(ctx, scope, "", galleryApplicationName) @@ -394,7 +395,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { }) t.Run("Search_PagerError", func(t *testing.T) { - mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationVersionsClient(ctrl) errorPager := &errorGalleryApplicationVersionsPager{} testClient := &testGalleryApplicationVersionsClient{ MockGalleryApplicationVersionsClient: mockClient, @@ -417,7 +418,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { }) t.Run("PotentialLinks", func(t *testing.T) { - mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationVersionsClient(ctrl) wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) links := wrapper.PotentialLinks() @@ -439,7 +440,7 @@ func TestComputeGalleryApplicationVersion(t *testing.T) { }) t.Run("ImplementsSearchableAdapter", func(t *testing.T) { - mockClient := NewMockGalleryApplicationVersionsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationVersionsClient(ctrl) wrapper := NewComputeGalleryApplicationVersion(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) diff --git a/sources/azure/manual/compute-gallery-application_test.go b/sources/azure/manual/compute-gallery-application_test.go index 57b35206..11ca6264 100644 --- a/sources/azure/manual/compute-gallery-application_test.go +++ b/sources/azure/manual/compute-gallery-application_test.go @@ -15,6 +15,7 @@ import ( "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/azure/shared/mocks" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -51,7 +52,7 @@ func (e *errorGalleryApplicationsPager) NextPage(ctx context.Context) (armcomput // testGalleryApplicationsClient wraps the mock and returns a pager from NewListByGalleryPager. type testGalleryApplicationsClient struct { - *MockGalleryApplicationsClient + *mocks.MockGalleryApplicationsClient pager clients.GalleryApplicationsPager } @@ -90,7 +91,7 @@ func TestComputeGalleryApplication(t *testing.T) { t.Run("Get", func(t *testing.T) { app := createAzureGalleryApplication(galleryApplicationName) - mockClient := NewMockGalleryApplicationsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationsClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryApplicationName, nil).Return( armcompute.GalleryApplicationsClientGetResponse{ GalleryApplication: *app, @@ -132,7 +133,7 @@ func TestComputeGalleryApplication(t *testing.T) { }) t.Run("Get_InvalidQueryParts", func(t *testing.T) { - mockClient := NewMockGalleryApplicationsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationsClient(ctrl) wrapper := NewComputeGalleryApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -143,7 +144,7 @@ func TestComputeGalleryApplication(t *testing.T) { }) t.Run("Get_EmptyGalleryName", func(t *testing.T) { - mockClient := NewMockGalleryApplicationsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationsClient(ctrl) wrapper := NewComputeGalleryApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -155,7 +156,7 @@ func TestComputeGalleryApplication(t *testing.T) { }) t.Run("Get_EmptyApplicationName", func(t *testing.T) { - mockClient := NewMockGalleryApplicationsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationsClient(ctrl) wrapper := NewComputeGalleryApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -168,7 +169,7 @@ func TestComputeGalleryApplication(t *testing.T) { t.Run("Get_ClientError", func(t *testing.T) { expectedErr := errors.New("application not found") - mockClient := NewMockGalleryApplicationsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationsClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, "nonexistent", nil).Return( armcompute.GalleryApplicationsClientGetResponse{}, expectedErr) @@ -186,7 +187,7 @@ func TestComputeGalleryApplication(t *testing.T) { app1 := createAzureGalleryApplication("app-1") app2 := createAzureGalleryApplication("app-2") - mockClient := NewMockGalleryApplicationsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationsClient(ctrl) pages := []armcompute.GalleryApplicationsClientListByGalleryResponse{ { GalleryApplicationList: armcompute.GalleryApplicationList{ @@ -225,7 +226,7 @@ func TestComputeGalleryApplication(t *testing.T) { }) t.Run("Search_InvalidQueryParts", func(t *testing.T) { - mockClient := NewMockGalleryApplicationsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationsClient(ctrl) wrapper := NewComputeGalleryApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -242,7 +243,7 @@ func TestComputeGalleryApplication(t *testing.T) { }) t.Run("Search_EmptyGalleryName", func(t *testing.T) { - mockClient := NewMockGalleryApplicationsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationsClient(ctrl) wrapper := NewComputeGalleryApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) _, qErr := wrapper.Search(ctx, scope, "") @@ -252,7 +253,7 @@ func TestComputeGalleryApplication(t *testing.T) { }) t.Run("Search_PagerError", func(t *testing.T) { - mockClient := NewMockGalleryApplicationsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationsClient(ctrl) errorPager := &errorGalleryApplicationsPager{} testClient := &testGalleryApplicationsClient{ MockGalleryApplicationsClient: mockClient, @@ -274,7 +275,7 @@ func TestComputeGalleryApplication(t *testing.T) { }) t.Run("PotentialLinks", func(t *testing.T) { - mockClient := NewMockGalleryApplicationsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationsClient(ctrl) wrapper := NewComputeGalleryApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) links := wrapper.PotentialLinks() @@ -293,7 +294,7 @@ func TestComputeGalleryApplication(t *testing.T) { }) t.Run("ImplementsSearchableAdapter", func(t *testing.T) { - mockClient := NewMockGalleryApplicationsClient(ctrl) + mockClient := mocks.NewMockGalleryApplicationsClient(ctrl) wrapper := NewComputeGalleryApplication(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) diff --git a/sources/azure/manual/compute-gallery-image_test.go b/sources/azure/manual/compute-gallery-image_test.go index 57d4e159..d2632cae 100644 --- a/sources/azure/manual/compute-gallery-image_test.go +++ b/sources/azure/manual/compute-gallery-image_test.go @@ -14,6 +14,7 @@ import ( "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/azure/shared/mocks" "github.com/overmindtech/cli/sources/shared" "github.com/overmindtech/cli/sources/stdlib" ) @@ -50,7 +51,7 @@ func (e *errorGalleryImagesPager) NextPage(ctx context.Context) (armcompute.Gall // testGalleryImagesClient wraps the mock and returns a pager from NewListByGalleryPager. type testGalleryImagesClient struct { - *MockGalleryImagesClient + *mocks.MockGalleryImagesClient pager clients.GalleryImagesPager } @@ -103,7 +104,7 @@ func TestComputeGalleryImage(t *testing.T) { t.Run("Get", func(t *testing.T) { image := createAzureGalleryImage(galleryImageName) - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( armcompute.GalleryImagesClientGetResponse{ GalleryImage: *image, @@ -146,7 +147,7 @@ func TestComputeGalleryImage(t *testing.T) { t.Run("GetWithURIs", func(t *testing.T) { image := createAzureGalleryImageWithURIs(galleryImageName) - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( armcompute.GalleryImagesClientGetResponse{ GalleryImage: *image, @@ -179,7 +180,7 @@ func TestComputeGalleryImage(t *testing.T) { image := createAzureGalleryImage(galleryImageName) image.Properties.Eula = new("This software is provided as-is. No warranty.") - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( armcompute.GalleryImagesClientGetResponse{ GalleryImage: *image, @@ -212,7 +213,7 @@ func TestComputeGalleryImage(t *testing.T) { image.Properties.PrivacyStatementURI = new("https://example.com/privacy") image.Properties.ReleaseNoteURI = new("https://example.com/release-notes") - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( armcompute.GalleryImagesClientGetResponse{ GalleryImage: *image, @@ -253,7 +254,7 @@ func TestComputeGalleryImage(t *testing.T) { image := createAzureGalleryImage(galleryImageName) image.Properties.PrivacyStatementURI = new("https://192.168.1.10:8443/privacy") - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, galleryImageName, nil).Return( armcompute.GalleryImagesClientGetResponse{ GalleryImage: *image, @@ -291,7 +292,7 @@ func TestComputeGalleryImage(t *testing.T) { }) t.Run("Get_InvalidQueryParts", func(t *testing.T) { - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -303,7 +304,7 @@ func TestComputeGalleryImage(t *testing.T) { }) t.Run("Get_EmptyGalleryName", func(t *testing.T) { - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -315,7 +316,7 @@ func TestComputeGalleryImage(t *testing.T) { }) t.Run("Get_EmptyImageName", func(t *testing.T) { - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -328,7 +329,7 @@ func TestComputeGalleryImage(t *testing.T) { t.Run("Get_ClientError", func(t *testing.T) { expectedErr := errors.New("image not found") - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) mockClient.EXPECT().Get(ctx, resourceGroup, galleryName, "nonexistent", nil).Return( armcompute.GalleryImagesClientGetResponse{}, expectedErr) @@ -346,7 +347,7 @@ func TestComputeGalleryImage(t *testing.T) { img1 := createAzureGalleryImage("image-1") img2 := createAzureGalleryImage("image-2") - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) pages := []armcompute.GalleryImagesClientListByGalleryResponse{ { GalleryImageList: armcompute.GalleryImageList{ @@ -385,7 +386,7 @@ func TestComputeGalleryImage(t *testing.T) { }) t.Run("Search_InvalidQueryParts", func(t *testing.T) { - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) @@ -403,7 +404,7 @@ func TestComputeGalleryImage(t *testing.T) { }) t.Run("Search_EmptyGalleryName", func(t *testing.T) { - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) _, qErr := wrapper.Search(ctx, scope, "") @@ -413,7 +414,7 @@ func TestComputeGalleryImage(t *testing.T) { }) t.Run("Search_PagerError", func(t *testing.T) { - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) errorPager := &errorGalleryImagesPager{} testClient := &testGalleryImagesClient{ MockGalleryImagesClient: mockClient, @@ -435,7 +436,7 @@ func TestComputeGalleryImage(t *testing.T) { }) t.Run("PotentialLinks", func(t *testing.T) { - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) links := wrapper.PotentialLinks() @@ -453,7 +454,7 @@ func TestComputeGalleryImage(t *testing.T) { }) t.Run("ImplementsSearchableAdapter", func(t *testing.T) { - mockClient := NewMockGalleryImagesClient(ctrl) + mockClient := mocks.NewMockGalleryImagesClient(ctrl) wrapper := NewComputeGalleryImage(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) diff --git a/sources/azure/manual/mock_gallery_application_versions_client_test.go b/sources/azure/manual/mock_gallery_application_versions_client_test.go deleted file mode 100644 index e86f52f3..00000000 --- a/sources/azure/manual/mock_gallery_application_versions_client_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: sources/azure/clients/gallery-application-versions-client.go -// -// Generated by this command: -// -// mockgen -destination=sources/azure/manual/mock_gallery_application_versions_client_test.go -package=manual -source=sources/azure/clients/gallery-application-versions-client.go -// - -// Package manual is a generated GoMock package. -package manual - -import ( - context "context" - reflect "reflect" - - armcompute "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" - clients "github.com/overmindtech/cli/sources/azure/clients" - gomock "go.uber.org/mock/gomock" -) - -// MockGalleryApplicationVersionsClient is a mock of GalleryApplicationVersionsClient interface. -type MockGalleryApplicationVersionsClient struct { - ctrl *gomock.Controller - recorder *MockGalleryApplicationVersionsClientMockRecorder - isgomock struct{} -} - -// MockGalleryApplicationVersionsClientMockRecorder is the mock recorder for MockGalleryApplicationVersionsClient. -type MockGalleryApplicationVersionsClientMockRecorder struct { - mock *MockGalleryApplicationVersionsClient -} - -// NewMockGalleryApplicationVersionsClient creates a new mock instance. -func NewMockGalleryApplicationVersionsClient(ctrl *gomock.Controller) *MockGalleryApplicationVersionsClient { - mock := &MockGalleryApplicationVersionsClient{ctrl: ctrl} - mock.recorder = &MockGalleryApplicationVersionsClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockGalleryApplicationVersionsClient) EXPECT() *MockGalleryApplicationVersionsClientMockRecorder { - return m.recorder -} - -// Get mocks base method. -func (m *MockGalleryApplicationVersionsClient) Get(ctx context.Context, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName string, options *armcompute.GalleryApplicationVersionsClientGetOptions) (armcompute.GalleryApplicationVersionsClientGetResponse, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options) - ret0, _ := ret[0].(armcompute.GalleryApplicationVersionsClientGetResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Get indicates an expected call of Get. -func (mr *MockGalleryApplicationVersionsClientMockRecorder) Get(ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleryApplicationVersionsClient)(nil).Get), ctx, resourceGroupName, galleryName, galleryApplicationName, galleryApplicationVersionName, options) -} - -// NewListByGalleryApplicationPager mocks base method. -func (m *MockGalleryApplicationVersionsClient) NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName string, options *armcompute.GalleryApplicationVersionsClientListByGalleryApplicationOptions) clients.GalleryApplicationVersionsPager { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewListByGalleryApplicationPager", resourceGroupName, galleryName, galleryApplicationName, options) - ret0, _ := ret[0].(clients.GalleryApplicationVersionsPager) - return ret0 -} - -// NewListByGalleryApplicationPager indicates an expected call of NewListByGalleryApplicationPager. -func (mr *MockGalleryApplicationVersionsClientMockRecorder) NewListByGalleryApplicationPager(resourceGroupName, galleryName, galleryApplicationName, options any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByGalleryApplicationPager", reflect.TypeOf((*MockGalleryApplicationVersionsClient)(nil).NewListByGalleryApplicationPager), resourceGroupName, galleryName, galleryApplicationName, options) -} diff --git a/sources/azure/manual/mock_gallery_applications_client_test.go b/sources/azure/manual/mock_gallery_applications_client_test.go deleted file mode 100644 index e317c794..00000000 --- a/sources/azure/manual/mock_gallery_applications_client_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: sources/azure/clients/gallery-applications-client.go -// -// Generated by this command: -// -// mockgen -destination=sources/azure/manual/mock_gallery_applications_client_test.go -package=manual -source=sources/azure/clients/gallery-applications-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" -) - -// MockGalleryApplicationsClient is a mock of GalleryApplicationsClient interface. -type MockGalleryApplicationsClient struct { - ctrl *gomock.Controller - recorder *MockGalleryApplicationsClientMockRecorder - isgomock struct{} -} - -// MockGalleryApplicationsClientMockRecorder is the mock recorder for MockGalleryApplicationsClient. -type MockGalleryApplicationsClientMockRecorder struct { - mock *MockGalleryApplicationsClient -} - -// NewMockGalleryApplicationsClient creates a new mock instance. -func NewMockGalleryApplicationsClient(ctrl *gomock.Controller) *MockGalleryApplicationsClient { - mock := &MockGalleryApplicationsClient{ctrl: ctrl} - mock.recorder = &MockGalleryApplicationsClientMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockGalleryApplicationsClient) EXPECT() *MockGalleryApplicationsClientMockRecorder { - return m.recorder -} - -// Get mocks base method. -func (m *MockGalleryApplicationsClient) Get(ctx context.Context, resourceGroupName, galleryName, galleryApplicationName string, options *armcompute.GalleryApplicationsClientGetOptions) (armcompute.GalleryApplicationsClientGetResponse, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, galleryName, galleryApplicationName, options) - ret0, _ := ret[0].(armcompute.GalleryApplicationsClientGetResponse) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Get indicates an expected call of Get. -func (mr *MockGalleryApplicationsClientMockRecorder) Get(ctx, resourceGroupName, galleryName, galleryApplicationName, options any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockGalleryApplicationsClient)(nil).Get), ctx, resourceGroupName, galleryName, galleryApplicationName, options) -} - -// NewListByGalleryPager mocks base method. -func (m *MockGalleryApplicationsClient) NewListByGalleryPager(resourceGroupName, galleryName string, options *armcompute.GalleryApplicationsClientListByGalleryOptions) clients.GalleryApplicationsPager { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewListByGalleryPager", resourceGroupName, galleryName, options) - ret0, _ := ret[0].(clients.GalleryApplicationsPager) - return ret0 -} - -// NewListByGalleryPager indicates an expected call of NewListByGalleryPager. -func (mr *MockGalleryApplicationsClientMockRecorder) NewListByGalleryPager(resourceGroupName, galleryName, options any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByGalleryPager", reflect.TypeOf((*MockGalleryApplicationsClient)(nil).NewListByGalleryPager), resourceGroupName, galleryName, options) -} diff --git a/sources/azure/manual/mock_gallery_images_client_test.go b/sources/azure/manual/mock_gallery_images_client_test.go deleted file mode 100644 index 58709319..00000000 --- a/sources/azure/manual/mock_gallery_images_client_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// 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/mocks/mock_dedicated_hosts_client.go b/sources/azure/shared/mocks/mock_dedicated_hosts_client.go new file mode 100644 index 00000000..a05b8cb5 --- /dev/null +++ b/sources/azure/shared/mocks/mock_dedicated_hosts_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: dedicated-hosts-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_dedicated_hosts_client.go -package=mocks -source=dedicated-hosts-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" +) + +// MockDedicatedHostsClient is a mock of DedicatedHostsClient interface. +type MockDedicatedHostsClient struct { + ctrl *gomock.Controller + recorder *MockDedicatedHostsClientMockRecorder + isgomock struct{} +} + +// MockDedicatedHostsClientMockRecorder is the mock recorder for MockDedicatedHostsClient. +type MockDedicatedHostsClientMockRecorder struct { + mock *MockDedicatedHostsClient +} + +// NewMockDedicatedHostsClient creates a new mock instance. +func NewMockDedicatedHostsClient(ctrl *gomock.Controller) *MockDedicatedHostsClient { + mock := &MockDedicatedHostsClient{ctrl: ctrl} + mock.recorder = &MockDedicatedHostsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDedicatedHostsClient) EXPECT() *MockDedicatedHostsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockDedicatedHostsClient) Get(ctx context.Context, resourceGroupName, hostGroupName, hostName string, options *armcompute.DedicatedHostsClientGetOptions) (armcompute.DedicatedHostsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, hostGroupName, hostName, options) + ret0, _ := ret[0].(armcompute.DedicatedHostsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockDedicatedHostsClientMockRecorder) Get(ctx, resourceGroupName, hostGroupName, hostName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDedicatedHostsClient)(nil).Get), ctx, resourceGroupName, hostGroupName, hostName, options) +} + +// NewListByHostGroupPager mocks base method. +func (m *MockDedicatedHostsClient) NewListByHostGroupPager(resourceGroupName, hostGroupName string, options *armcompute.DedicatedHostsClientListByHostGroupOptions) clients.DedicatedHostsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByHostGroupPager", resourceGroupName, hostGroupName, options) + ret0, _ := ret[0].(clients.DedicatedHostsPager) + return ret0 +} + +// NewListByHostGroupPager indicates an expected call of NewListByHostGroupPager. +func (mr *MockDedicatedHostsClientMockRecorder) NewListByHostGroupPager(resourceGroupName, hostGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByHostGroupPager", reflect.TypeOf((*MockDedicatedHostsClient)(nil).NewListByHostGroupPager), resourceGroupName, hostGroupName, options) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 551d06e5..2880bf50 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -39,6 +39,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-compute-gallery-application-version": {"galleries", "applications", "versions"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{applicationName}/versions/{versionName}", "azure-compute-gallery-application": {"galleries", "applications"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{applicationName}", "azure-compute-gallery-image": {"galleries", "images"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/images/{imageName}", + "azure-compute-dedicated-host": {"hostGroups", "hosts"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/hostGroups/{hostGroupName}/hosts/{hostName}", "azure-network-subnet": {"virtualNetworks", "subnets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName}", "azure-network-virtual-network-peering": {"virtualNetworks", "virtualNetworkPeerings"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/virtualNetworkPeerings/{peeringName}", "azure-network-route": {"routeTables", "routes"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/routeTables/{routeTableName}/routes/{routeName}", From f7a8c8500073e23edfccdb15e4f71cfad55be453 Mon Sep 17 00:00:00 2001 From: carabasdaniel Date: Tue, 3 Mar 2026 15:42:11 +0200 Subject: [PATCH 41/74] Overmind knowledge list (#4079) Add `overmind knowledge list` CLI command to show knowledge files discovered from the current location. This command provides visibility into which knowledge files `terraform plan` would use, helping engineers understand and debug their knowledge setup. It reuses existing discovery logic and includes detailed output for valid files and warnings. --- Linear Issue: [ENG-2920](https://linear.app/overmind/issue/ENG-2920/add-overmind-knowledge-list-cli-command)

Open in Web Open in Cursor 

--------- Co-authored-by: Cursor Agent Co-authored-by: carabasdaniel GitOrigin-RevId: ef72a8380438a0f5da601407251bd5bbf192709a --- README.md | 8 + cmd/knowledge.go | 24 +++ cmd/knowledge_list.go | 106 ++++++++++ cmd/knowledge_list_test.go | 392 +++++++++++++++++++++++++++++++++++++ 4 files changed, 530 insertions(+) create mode 100644 cmd/knowledge.go create mode 100644 cmd/knowledge_list.go create mode 100644 cmd/knowledge_list_test.go diff --git a/README.md b/README.md index 4a75128a..e2f6c167 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,14 @@ overmind --version changes you make with `overmind terraform apply`, so that you can be sure that your changes haven't had any unexpected downstream impact. +- `overmind knowledge list` + + View which knowledge files Overmind would discover from your current location. + Knowledge files in `.overmind/knowledge/` teach the AI investigator about your + infrastructure context, standards, and approved patterns. This command shows the + resolved knowledge directory path, valid files with their metadata, and any + validation warnings for invalid files. + ## Cloud Provider Support The CLI automatically discovers AWS and GCP providers from your Terraform configuration. diff --git a/cmd/knowledge.go b/cmd/knowledge.go new file mode 100644 index 00000000..b0e6285a --- /dev/null +++ b/cmd/knowledge.go @@ -0,0 +1,24 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +// knowledgeCmd represents the knowledge command +var knowledgeCmd = &cobra.Command{ + Use: "knowledge", + GroupID: "iac", + Short: "Manage tribal knowledge files used for change analysis", + Long: `Knowledge files in .overmind/knowledge/ help Overmind understand your infrastructure +context, giving better change analysis and risk assessment. + +The 'list' subcommand shows which knowledge files Overmind would discover from your +current location, using the same logic as 'overmind terraform plan'.`, + Run: func(cmd *cobra.Command, args []string) { + _ = cmd.Help() + }, +} + +func init() { + rootCmd.AddCommand(knowledgeCmd) +} diff --git a/cmd/knowledge_list.go b/cmd/knowledge_list.go new file mode 100644 index 00000000..6ab4c8f1 --- /dev/null +++ b/cmd/knowledge_list.go @@ -0,0 +1,106 @@ +package cmd + +import ( + "fmt" + "strings" + + "github.com/overmindtech/pterm" + "github.com/overmindtech/cli/knowledge" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// knowledgeListCmd represents the knowledge list command +var knowledgeListCmd = &cobra.Command{ + Use: "list", + Short: "Lists knowledge files that would be used from the current location", + PreRun: PreRunSetup, + RunE: KnowledgeList, +} + +func KnowledgeList(cmd *cobra.Command, args []string) error { + startDir := viper.GetString("dir") + output, err := renderKnowledgeList(startDir) + if err != nil { + return err + } + + fmt.Print(output) + return nil +} + +// renderKnowledgeList handles the knowledge list logic and returns formatted output. +// This is separated from the command for testability. +func renderKnowledgeList(startDir string) (string, error) { + var output strings.Builder + + knowledgeDir := knowledge.FindKnowledgeDir(startDir) + + if knowledgeDir == "" { + output.WriteString(pterm.Info.Sprint("No .overmind/knowledge/ directory found from current location\n\n")) + output.WriteString("Knowledge files help Overmind understand your infrastructure context.\n") + output.WriteString("Create a .overmind/knowledge/ directory to add knowledge files.\n") + output.WriteString("Without knowledge files, 'terraform plan' will proceed with standard analysis.\n") + return output.String(), nil + } + + files, warnings := knowledge.Discover(knowledgeDir) + + // Show resolved directory + output.WriteString(pterm.Info.Sprintf("Knowledge directory: %s\n\n", knowledgeDir)) + + // Show valid files + if len(files) > 0 { + output.WriteString(pterm.DefaultHeader.Sprint("Valid Knowledge Files") + "\n\n") + + // Create table data + tableData := pterm.TableData{ + {"Name", "Description", "File Path"}, + } + + for _, f := range files { + tableData = append(tableData, []string{ + f.Name, + truncateDescription(f.Description, 60), + f.FileName, + }) + } + + table, err := pterm.DefaultTable.WithHasHeader().WithData(tableData).Srender() + if err != nil { + return "", fmt.Errorf("failed to render table: %w", err) + } + output.WriteString(table) + output.WriteString("\n") + } else if len(warnings) == 0 { + output.WriteString(pterm.Info.Sprint("No knowledge files found\n\n")) + } + + // Show warnings + if len(warnings) > 0 { + output.WriteString(pterm.DefaultHeader.Sprint("Invalid/Skipped Files") + "\n\n") + + for _, w := range warnings { + output.WriteString(pterm.Warning.Sprintf(" %s\n", w.Path)) + fmt.Fprintf(&output, " Reason: %s\n", w.Reason) + } + output.WriteString("\n") + } + + return output.String(), nil +} + +// truncateDescription truncates a description to maxLen characters, adding "..." if truncated +func truncateDescription(desc string, maxLen int) string { + if len(desc) <= maxLen { + return desc + } + return desc[:maxLen-3] + "..." +} + +func init() { + knowledgeCmd.AddCommand(knowledgeListCmd) + + knowledgeListCmd.Flags().String("dir", ".", "Directory to start searching from") + knowledgeListCmd.Flags().MarkHidden("dir") //nolint:errcheck // not possible to error +} diff --git a/cmd/knowledge_list_test.go b/cmd/knowledge_list_test.go new file mode 100644 index 00000000..26b4dae4 --- /dev/null +++ b/cmd/knowledge_list_test.go @@ -0,0 +1,392 @@ +package cmd + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestRenderKnowledgeList_NoKnowledgeDir(t *testing.T) { + dir := t.TempDir() + + output, err := renderKnowledgeList(dir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !strings.Contains(output, "No .overmind/knowledge/ directory found") { + t.Errorf("expected message about no directory found, got: %s", output) + } + if !strings.Contains(output, "Create a .overmind/knowledge/ directory") { + t.Errorf("expected helpful message about creating directory, got: %s", output) + } + if !strings.Contains(output, "terraform plan") { + t.Errorf("expected reference to terraform plan, got: %s", output) + } +} + +func TestRenderKnowledgeList_EmptyKnowledgeDir(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, ".overmind", "knowledge") + err := os.MkdirAll(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + output, err := renderKnowledgeList(dir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if !strings.Contains(output, "Knowledge directory:") { + t.Errorf("expected resolved directory message, got: %s", output) + } + if !strings.Contains(output, knowledgeDir) { + t.Errorf("expected directory path %s in output, got: %s", knowledgeDir, output) + } + if !strings.Contains(output, "No knowledge files found") { + t.Errorf("expected 'No knowledge files found' message, got: %s", output) + } +} + +func TestRenderKnowledgeList_ValidFiles(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, ".overmind", "knowledge") + err := os.MkdirAll(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create valid knowledge files + writeTestFile(t, filepath.Join(knowledgeDir, "aws-s3.md"), `--- +name: aws-s3-security +description: Security best practices for S3 buckets +--- +# AWS S3 Security +Content here. +`) + + subdir := filepath.Join(knowledgeDir, "cloud") + err = os.Mkdir(subdir, 0755) + if err != nil { + t.Fatal(err) + } + writeTestFile(t, filepath.Join(subdir, "gcp.md"), `--- +name: gcp-compute +description: GCP Compute Engine guidelines +--- +# GCP Compute +Content here. +`) + + output, err := renderKnowledgeList(dir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Check for resolved directory + if !strings.Contains(output, "Knowledge directory:") { + t.Errorf("expected resolved directory message, got: %s", output) + } + if !strings.Contains(output, knowledgeDir) { + t.Errorf("expected directory path in output, got: %s", output) + } + + // Check for header + if !strings.Contains(output, "Valid Knowledge Files") { + t.Errorf("expected 'Valid Knowledge Files' header, got: %s", output) + } + + // Check for first file details + if !strings.Contains(output, "aws-s3-security") { + t.Errorf("expected file name 'aws-s3-security', got: %s", output) + } + if !strings.Contains(output, "Security best practices for S3 buckets") { + t.Errorf("expected file description, got: %s", output) + } + if !strings.Contains(output, "aws-s3.md") { + t.Errorf("expected file path 'aws-s3.md', got: %s", output) + } + + // Check for second file details + if !strings.Contains(output, "gcp-compute") { + t.Errorf("expected file name 'gcp-compute', got: %s", output) + } + if !strings.Contains(output, "GCP Compute Engine guidelines") { + t.Errorf("expected file description, got: %s", output) + } + if !strings.Contains(output, filepath.Join("cloud", "gcp.md")) { + t.Errorf("expected file path 'cloud/gcp.md', got: %s", output) + } +} + +func TestRenderKnowledgeList_InvalidFiles(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, ".overmind", "knowledge") + err := os.MkdirAll(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create valid file + writeTestFile(t, filepath.Join(knowledgeDir, "valid.md"), `--- +name: valid-file +description: A valid knowledge file +--- +Content here. +`) + + // Create invalid file (missing frontmatter) + writeTestFile(t, filepath.Join(knowledgeDir, "invalid.md"), `# No frontmatter +This file is missing frontmatter. +`) + + output, err := renderKnowledgeList(dir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Check for valid file + if !strings.Contains(output, "Valid Knowledge Files") { + t.Errorf("expected 'Valid Knowledge Files' header, got: %s", output) + } + if !strings.Contains(output, "valid-file") { + t.Errorf("expected valid file name, got: %s", output) + } + + // Check for warnings section + if !strings.Contains(output, "Invalid/Skipped Files") { + t.Errorf("expected 'Invalid/Skipped Files' header, got: %s", output) + } + if !strings.Contains(output, "invalid.md") { + t.Errorf("expected invalid file path in warnings, got: %s", output) + } + if !strings.Contains(output, "Reason:") { + t.Errorf("expected reason in warnings, got: %s", output) + } +} + +func TestRenderKnowledgeList_OnlyInvalidFiles(t *testing.T) { + dir := t.TempDir() + knowledgeDir := filepath.Join(dir, ".overmind", "knowledge") + err := os.MkdirAll(knowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create only invalid files + writeTestFile(t, filepath.Join(knowledgeDir, "bad1.md"), `# No frontmatter`) + writeTestFile(t, filepath.Join(knowledgeDir, "bad2.md"), `--- +name: invalid name with spaces +description: This has an invalid name +--- +Content. +`) + + output, err := renderKnowledgeList(dir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Should NOT have valid files section + if strings.Contains(output, "Valid Knowledge Files") { + t.Errorf("should not have 'Valid Knowledge Files' header when all files are invalid, got: %s", output) + } + + // Should have warnings + if !strings.Contains(output, "Invalid/Skipped Files") { + t.Errorf("expected 'Invalid/Skipped Files' header, got: %s", output) + } + if !strings.Contains(output, "bad1.md") { + t.Errorf("expected bad1.md in warnings, got: %s", output) + } + if !strings.Contains(output, "bad2.md") { + t.Errorf("expected bad2.md in warnings, got: %s", output) + } +} + +func TestRenderKnowledgeList_SubdirectoryUsesLocal(t *testing.T) { + dir := t.TempDir() + + // Create parent knowledge dir + parentKnowledgeDir := filepath.Join(dir, ".overmind", "knowledge") + err := os.MkdirAll(parentKnowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + writeTestFile(t, filepath.Join(parentKnowledgeDir, "parent.md"), `--- +name: parent-file +description: Parent knowledge file +--- +Content. +`) + + // Create subdirectory with its own knowledge dir + childDir := filepath.Join(dir, "child") + childKnowledgeDir := filepath.Join(childDir, ".overmind", "knowledge") + err = os.MkdirAll(childKnowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + writeTestFile(t, filepath.Join(childKnowledgeDir, "child.md"), `--- +name: child-file +description: Child knowledge file +--- +Content. +`) + + output, err := renderKnowledgeList(childDir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Should use child knowledge dir + if !strings.Contains(output, childKnowledgeDir) { + t.Errorf("expected child knowledge dir %s, got: %s", childKnowledgeDir, output) + } + if strings.Contains(output, parentKnowledgeDir) { + t.Errorf("should not mention parent knowledge dir, got: %s", output) + } + + // Should show child file, not parent file + if !strings.Contains(output, "child-file") { + t.Errorf("expected child file, got: %s", output) + } + if strings.Contains(output, "parent-file") { + t.Errorf("should not show parent file, got: %s", output) + } +} + +func TestRenderKnowledgeList_SubdirectoryUsesParent(t *testing.T) { + dir := t.TempDir() + + // Create parent knowledge dir + parentKnowledgeDir := filepath.Join(dir, ".overmind", "knowledge") + err := os.MkdirAll(parentKnowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + writeTestFile(t, filepath.Join(parentKnowledgeDir, "parent.md"), `--- +name: parent-file +description: Parent knowledge file +--- +Content. +`) + + // Create subdirectory WITHOUT its own knowledge dir + childDir := filepath.Join(dir, "child") + err = os.Mkdir(childDir, 0755) + if err != nil { + t.Fatal(err) + } + + output, err := renderKnowledgeList(childDir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Should use parent knowledge dir + if !strings.Contains(output, parentKnowledgeDir) { + t.Errorf("expected parent knowledge dir %s, got: %s", parentKnowledgeDir, output) + } + + // Should show parent file + if !strings.Contains(output, "parent-file") { + t.Errorf("expected parent file, got: %s", output) + } +} + +func TestRenderKnowledgeList_StopsAtGitBoundary(t *testing.T) { + dir := t.TempDir() + + // Create outer directory with knowledge (outside git repo) + outerKnowledgeDir := filepath.Join(dir, ".overmind", "knowledge") + err := os.MkdirAll(outerKnowledgeDir, 0755) + if err != nil { + t.Fatal(err) + } + writeTestFile(t, filepath.Join(outerKnowledgeDir, "outer.md"), `--- +name: outer-file +description: Knowledge file outside git repo +--- +Content. +`) + + // Create a git repo subdirectory + repoDir := filepath.Join(dir, "my-repo") + repoGitDir := filepath.Join(repoDir, ".git") + err = os.MkdirAll(repoGitDir, 0755) + if err != nil { + t.Fatal(err) + } + + // Create a workspace dir inside the repo (without its own knowledge) + workspaceDir := filepath.Join(repoDir, "workspace") + err = os.Mkdir(workspaceDir, 0755) + if err != nil { + t.Fatal(err) + } + + output, err := renderKnowledgeList(workspaceDir) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Should NOT find outer knowledge dir (stops at .git boundary) + if !strings.Contains(output, "No .overmind/knowledge/ directory found") { + t.Errorf("expected no knowledge dir found (should stop at .git), got: %s", output) + } + if strings.Contains(output, "outer-file") { + t.Errorf("should not find knowledge from outside git repo, got: %s", output) + } +} + +func TestTruncateDescription(t *testing.T) { + tests := []struct { + name string + desc string + maxLen int + expected string + }{ + { + name: "short description", + desc: "Short", + maxLen: 20, + expected: "Short", + }, + { + name: "exact length", + desc: "Exactly twenty char", + maxLen: 20, + expected: "Exactly twenty char", + }, + { + name: "needs truncation", + desc: "This is a very long description that needs to be truncated", + maxLen: 20, + expected: "This is a very lo...", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := truncateDescription(tt.desc, tt.maxLen) + if result != tt.expected { + t.Errorf("expected %q, got %q", tt.expected, result) + } + if len(result) > tt.maxLen { + t.Errorf("result length %d exceeds maxLen %d", len(result), tt.maxLen) + } + }) + } +} + +// Helper function for tests +func writeTestFile(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 a12ce90cee537f098f0bff09d2c147af30a744d6 Mon Sep 17 00:00:00 2001 From: Elliot Waddington Date: Tue, 3 Mar 2026 14:43:26 +0100 Subject: [PATCH 42/74] ENG-2778 update and automate outdated screenshots and instructions on docs (#4064) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changed Pages ### Account | Page | Changes | | --- | --- | | [/account/teams](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/account/teams) | Fixed "a unlimited" → "an unlimited". Rewrote invite steps: direct link to `/settings/team`, corrected flow to match current UI (Invite button → dialog → email field). Updated managing members to reference ellipsis button. New screenshots. | | [/account/account_deletion](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/account/account_deletion) | Rewrote deletion steps: added "Remove Other Team Members" section with team settings link, corrected delete flow to reference ellipsis/revoke. New screenshots including leave-team and delete-confirmation. | ### Explore | Page | Changes | | --- | --- | | [/explore/usage](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/explore/usage) | Fixed typos: "query's" → "queries", "ect" → "etc.", "specefic" → "specific", "Lets" → "Let's", "the the" → "the". Renamed misspelled image file. New screenshots. | | [/explore/assistant](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/explore/assistant) | Fixed typo: "ect" → "etc." | ### Sources | Page | Changes | | --- | --- | | [/sources/aws/configuration](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/sources/aws/configuration) | Corrected nav from "profile picture in the top right → Account Settings" to "avatar in the sidebar → Sources". Fixed source settings URL. Updated alt text. New screenshots. | | [/sources/k8s/configuration](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/sources/k8s/configuration) | Changed "Account settings > API Keys" → "Settings › API Keys". Updated alt text. New screenshots. | ### Signals | Page | Changes | | --- | --- | | [/signals/routine](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/signals/routine) | Fixed typo "specefic" → "specific". Updated alt text. New screenshots. | | [/signals/overview](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/signals/overview) | New screenshots for `signals.png` and `custom_signal.png`. | ### Integrations | Page | Changes | | --- | --- | | [/integrations/terraform_enterprise](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/integrations/terraform_enterprise) | Fixed "a a" → "a", "a Overmind" → "an Overmind". Corrected outdated nav instructions. New screenshots for `om_suggested_values.png` and `om_disable.png`. | | [/integrations/github_actions](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/integrations/github_actions) | Changed "Account Settings > API Keys" → "Settings › API Keys". | | [/integrations/build_your_own](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/integrations/build_your_own) | Changed "Account Settings > API Keys" → "Settings › API Keys". | | [/integrations/opa-conftest](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/integrations/opa-conftest) | Fixed missing space after a link. New `policies.png` screenshot. | | [/integrations/github_app](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/integrations/github_app) | New `github-app.png` screenshot. | ### Resource Management | Page | Changes | | --- | --- | | [/resource_management/overview](https://docs-overmind-tech-git-eng-2778-update-outdated-screensh-7bac7e.preview.df.overmind-demo.com/resource_management/overview) | Improved alt text. New screenshots for all 6 auto label images. | --- > [!NOTE] > **Low Risk** > Low risk documentation-only changes; main risk is broken links or mismatched screenshots after updating UI flows and renaming image assets. > > **Overview** > Updates multiple docs pages to match the current Overmind UI: rewrites **team invites/member management** and **account deletion** steps to use direct settings URLs and ellipsis-menu actions, and refreshes screenshot/alt text accordingly. > > Cleans up docs formatting and copy across integrations, signals, explore, and Terraform/CLI pages (typos, markdown tables/code blocks, consistent italics). Adds Playwright output directories to `.gitignore` and reformats several JSON metadata files under `docs/sources/*/data` for readability. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6ae78a0a859ea92de5ff7528ead9e2caca6e3223. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 9e01603de63c5c0ec61f8f4d7d02cf3e0b0b1ccf --- .../docs/sources/aws/account_settings.png | Bin 45804 -> 403511 bytes .../docs/sources/aws/aws_source_settings.png | Bin 116537 -> 127756 bytes .../docs/sources/aws/configuration.md | 12 ++-- .../docs/sources/aws/configure-aws.png | Bin 56460 -> 168857 bytes ...ansit-gateway-route-table-association.json | 26 +++++++- ...ansit-gateway-route-table-propagation.json | 27 +++++++- .../data/ec2-transit-gateway-route-table.json | 23 ++++++- .../aws/data/ec2-transit-gateway-route.json | 27 +++++++- .../docs/sources/aws/terraform.md | 20 +++--- .../sources/aws/update-to-pod-identity.md | 60 +++++++++--------- .../docs/sources/azure/_category_.json | 10 ++- .../gcp-ai-platform-batch-prediction-job.md | 6 +- .../gcp/Types/gcp-ai-platform-custom-job.md | 6 +- .../gcp/Types/gcp-ai-platform-endpoint.md | 8 +-- ...latform-model-deployment-monitoring-job.md | 6 +- .../gcp/Types/gcp-ai-platform-model.md | 6 +- .../gcp/Types/gcp-ai-platform-pipeline-job.md | 6 +- .../gcp-artifact-registry-docker-image.md | 8 +-- ...big-query-data-transfer-transfer-config.md | 8 +-- .../gcp/Types/gcp-big-query-dataset.md | 14 ++-- .../gcp/Types/gcp-big-query-routine.md | 8 +-- .../sources/gcp/Types/gcp-big-query-table.md | 14 ++-- .../Types/gcp-big-table-admin-app-profile.md | 8 +-- .../gcp/Types/gcp-big-table-admin-backup.md | 6 +- .../gcp/Types/gcp-big-table-admin-cluster.md | 6 +- .../gcp/Types/gcp-big-table-admin-instance.md | 14 ++-- .../gcp/Types/gcp-big-table-admin-table.md | 14 ++-- .../gcp-certificate-manager-certificate.md | 8 +-- .../Types/gcp-cloud-billing-billing-info.md | 6 +- .../gcp/Types/gcp-cloud-build-build.md | 6 +- .../gcp/Types/gcp-cloud-functions-function.md | 6 +- .../Types/gcp-cloud-kms-crypto-key-version.md | 8 +-- .../gcp/Types/gcp-cloud-kms-crypto-key.md | 8 +-- .../gcp/Types/gcp-cloud-kms-key-ring.md | 8 +-- .../gcp-cloud-resource-manager-project.md | 8 +-- .../gcp-cloud-resource-manager-tag-value.md | 8 +-- .../sources/gcp/Types/gcp-compute-address.md | 8 +-- .../gcp/Types/gcp-compute-autoscaler.md | 8 +-- .../gcp/Types/gcp-compute-backend-service.md | 10 +-- .../sources/gcp/Types/gcp-compute-disk.md | 8 +-- .../Types/gcp-compute-external-vpn-gateway.md | 8 +-- .../sources/gcp/Types/gcp-compute-firewall.md | 8 +-- .../gcp/Types/gcp-compute-forwarding-rule.md | 8 +-- .../gcp/Types/gcp-compute-global-address.md | 10 +-- .../gcp-compute-global-forwarding-rule.md | 8 +-- .../gcp/Types/gcp-compute-health-check.md | 10 +-- .../Types/gcp-compute-http-health-check.md | 8 +-- .../sources/gcp/Types/gcp-compute-image.md | 8 +-- .../gcp-compute-instance-group-manager.md | 8 +-- .../gcp/Types/gcp-compute-instance-group.md | 8 +-- .../Types/gcp-compute-instance-template.md | 8 +-- .../sources/gcp/Types/gcp-compute-instance.md | 8 +-- .../gcp/Types/gcp-compute-instant-snapshot.md | 8 +-- .../gcp/Types/gcp-compute-machine-image.md | 8 +-- .../gcp-compute-network-endpoint-group.md | 8 +-- .../sources/gcp/Types/gcp-compute-network.md | 8 +-- .../gcp/Types/gcp-compute-node-group.md | 10 +-- .../gcp/Types/gcp-compute-node-template.md | 8 +-- .../sources/gcp/Types/gcp-compute-project.md | 22 +++---- .../gcp-compute-public-delegated-prefix.md | 8 +-- .../Types/gcp-compute-region-commitment.md | 8 +-- ...compute-regional-instance-group-manager.md | 8 +-- .../gcp/Types/gcp-compute-reservation.md | 8 +-- .../sources/gcp/Types/gcp-compute-route.md | 8 +-- .../sources/gcp/Types/gcp-compute-router.md | 8 +-- .../gcp/Types/gcp-compute-security-policy.md | 8 +-- .../sources/gcp/Types/gcp-compute-snapshot.md | 8 +-- .../gcp/Types/gcp-compute-ssl-certificate.md | 8 +-- .../gcp/Types/gcp-compute-ssl-policy.md | 8 +-- .../gcp/Types/gcp-compute-subnetwork.md | 8 +-- .../Types/gcp-compute-target-http-proxy.md | 8 +-- .../Types/gcp-compute-target-https-proxy.md | 8 +-- .../gcp/Types/gcp-compute-target-pool.md | 8 +-- .../sources/gcp/Types/gcp-compute-url-map.md | 8 +-- .../gcp/Types/gcp-compute-vpn-gateway.md | 8 +-- .../gcp/Types/gcp-compute-vpn-tunnel.md | 8 +-- .../gcp/Types/gcp-container-cluster.md | 8 +-- .../gcp/Types/gcp-container-node-pool.md | 10 +-- .../gcp/Types/gcp-dataform-repository.md | 8 +-- .../gcp/Types/gcp-dataplex-aspect-type.md | 8 +-- .../gcp/Types/gcp-dataplex-data-scan.md | 8 +-- .../gcp/Types/gcp-dataplex-entry-group.md | 8 +-- .../Types/gcp-dataproc-autoscaling-policy.md | 8 +-- .../sources/gcp/Types/gcp-dataproc-cluster.md | 8 +-- .../sources/gcp/Types/gcp-dns-managed-zone.md | 8 +-- .../Types/gcp-essential-contacts-contact.md | 8 +-- .../sources/gcp/Types/gcp-file-instance.md | 8 +-- .../docs/sources/gcp/Types/gcp-iam-role.md | 6 +- .../gcp/Types/gcp-iam-service-account-key.md | 8 +-- .../gcp/Types/gcp-iam-service-account.md | 10 +-- .../sources/gcp/Types/gcp-logging-bucket.md | 6 +- .../sources/gcp/Types/gcp-logging-link.md | 10 +-- .../gcp/Types/gcp-logging-saved-query.md | 6 +- .../sources/gcp/Types/gcp-logging-sink.md | 6 +- .../gcp/Types/gcp-monitoring-alert-policy.md | 8 +-- .../Types/gcp-monitoring-custom-dashboard.md | 8 +-- .../gcp-monitoring-notification-channel.md | 8 +-- .../sources/gcp/Types/gcp-orgpolicy-policy.md | 8 +-- .../gcp/Types/gcp-pub-sub-subscription.md | 14 ++-- .../sources/gcp/Types/gcp-pub-sub-topic.md | 14 ++-- .../sources/gcp/Types/gcp-redis-instance.md | 8 +-- .../sources/gcp/Types/gcp-run-revision.md | 6 +- .../docs/sources/gcp/Types/gcp-run-service.md | 8 +-- .../gcp/Types/gcp-secret-manager-secret.md | 8 +-- ...nter-management-security-center-service.md | 6 +- .../Types/gcp-service-directory-endpoint.md | 8 +-- .../gcp/Types/gcp-service-usage-service.md | 6 +- .../sources/gcp/Types/gcp-spanner-database.md | 8 +-- .../sources/gcp/Types/gcp-spanner-instance.md | 8 +-- .../gcp/Types/gcp-sql-admin-backup-run.md | 6 +- .../sources/gcp/Types/gcp-sql-admin-backup.md | 6 +- .../gcp/Types/gcp-sql-admin-instance.md | 8 +-- .../Types/gcp-storage-bucket-iam-policy.md | 12 ++-- .../sources/gcp/Types/gcp-storage-bucket.md | 14 ++-- .../gcp-storage-transfer-transfer-job.md | 8 +-- .../docs/sources/gcp/configuration.md | 2 +- .../gcp-ai-platform-batch-prediction-job.json | 2 +- .../gcp/data/gcp-ai-platform-custom-job.json | 2 +- .../gcp/data/gcp-ai-platform-endpoint.json | 2 +- ...tform-model-deployment-monitoring-job.json | 2 +- .../gcp/data/gcp-ai-platform-model.json | 2 +- .../data/gcp-ai-platform-pipeline-job.json | 2 +- .../gcp-artifact-registry-docker-image.json | 2 +- ...g-query-data-transfer-transfer-config.json | 2 +- .../gcp/data/gcp-big-query-dataset.json | 2 +- .../gcp/data/gcp-big-query-routine.json | 7 +- .../sources/gcp/data/gcp-big-query-table.json | 2 +- .../data/gcp-big-table-admin-app-profile.json | 2 +- .../gcp/data/gcp-big-table-admin-backup.json | 2 +- .../gcp/data/gcp-big-table-admin-cluster.json | 2 +- .../data/gcp-big-table-admin-instance.json | 6 +- .../gcp/data/gcp-big-table-admin-table.json | 2 +- .../gcp-certificate-manager-certificate.json | 2 +- .../data/gcp-cloud-billing-billing-info.json | 6 +- .../gcp/data/gcp-cloud-build-build.json | 2 +- .../data/gcp-cloud-functions-function.json | 2 +- .../gcp-cloud-kms-crypto-key-version.json | 6 +- .../gcp/data/gcp-cloud-kms-crypto-key.json | 2 +- .../gcp/data/gcp-cloud-kms-key-ring.json | 6 +- .../gcp-cloud-resource-manager-project.json | 2 +- .../gcp-cloud-resource-manager-tag-value.json | 2 +- .../sources/gcp/data/gcp-compute-address.json | 2 +- .../gcp/data/gcp-compute-autoscaler.json | 6 +- .../gcp/data/gcp-compute-backend-service.json | 2 +- .../sources/gcp/data/gcp-compute-disk.json | 2 +- .../gcp-compute-external-vpn-gateway.json | 2 +- .../gcp/data/gcp-compute-firewall.json | 2 +- .../gcp/data/gcp-compute-forwarding-rule.json | 2 +- .../gcp/data/gcp-compute-global-address.json | 2 +- .../gcp-compute-global-forwarding-rule.json | 2 +- .../gcp/data/gcp-compute-health-check.json | 2 +- .../data/gcp-compute-http-health-check.json | 2 +- .../sources/gcp/data/gcp-compute-image.json | 2 +- .../gcp-compute-instance-group-manager.json | 2 +- .../gcp/data/gcp-compute-instance-group.json | 7 +- .../data/gcp-compute-instance-template.json | 2 +- .../gcp/data/gcp-compute-instance.json | 2 +- .../data/gcp-compute-instant-snapshot.json | 6 +- .../gcp/data/gcp-compute-machine-image.json | 2 +- .../gcp-compute-network-endpoint-group.json | 2 +- .../sources/gcp/data/gcp-compute-network.json | 7 +- .../gcp/data/gcp-compute-node-group.json | 6 +- .../gcp/data/gcp-compute-node-template.json | 6 +- .../sources/gcp/data/gcp-compute-project.json | 7 +- .../gcp-compute-public-delegated-prefix.json | 2 +- .../data/gcp-compute-region-commitment.json | 6 +- ...mpute-regional-instance-group-manager.json | 2 +- .../gcp/data/gcp-compute-reservation.json | 6 +- .../sources/gcp/data/gcp-compute-route.json | 2 +- .../sources/gcp/data/gcp-compute-router.json | 2 +- .../gcp/data/gcp-compute-security-policy.json | 2 +- .../gcp/data/gcp-compute-snapshot.json | 2 +- .../gcp/data/gcp-compute-ssl-certificate.json | 2 +- .../gcp/data/gcp-compute-ssl-policy.json | 2 +- .../gcp/data/gcp-compute-subnetwork.json | 2 +- .../data/gcp-compute-target-http-proxy.json | 6 +- .../data/gcp-compute-target-https-proxy.json | 2 +- .../gcp/data/gcp-compute-target-pool.json | 2 +- .../sources/gcp/data/gcp-compute-url-map.json | 6 +- .../gcp/data/gcp-compute-vpn-gateway.json | 6 +- .../gcp/data/gcp-compute-vpn-tunnel.json | 2 +- .../gcp/data/gcp-container-cluster.json | 2 +- .../gcp/data/gcp-container-node-pool.json | 2 +- .../gcp/data/gcp-dataform-repository.json | 2 +- .../gcp/data/gcp-dataplex-aspect-type.json | 2 +- .../gcp/data/gcp-dataplex-data-scan.json | 7 +- .../gcp/data/gcp-dataplex-entry-group.json | 2 +- .../data/gcp-dataproc-autoscaling-policy.json | 2 +- .../gcp/data/gcp-dataproc-cluster.json | 2 +- .../gcp/data/gcp-dns-managed-zone.json | 7 +- .../data/gcp-essential-contacts-contact.json | 2 +- .../sources/gcp/data/gcp-file-instance.json | 7 +- .../docs/sources/gcp/data/gcp-iam-role.json | 2 +- .../gcp/data/gcp-iam-service-account-key.json | 6 +- .../gcp/data/gcp-iam-service-account.json | 2 +- .../sources/gcp/data/gcp-logging-bucket.json | 2 +- .../sources/gcp/data/gcp-logging-link.json | 7 +- .../gcp/data/gcp-logging-saved-query.json | 2 +- .../sources/gcp/data/gcp-logging-sink.json | 2 +- .../gcp/data/gcp-monitoring-alert-policy.json | 6 +- .../data/gcp-monitoring-custom-dashboard.json | 2 +- .../gcp-monitoring-notification-channel.json | 6 +- .../gcp/data/gcp-orgpolicy-policy.json | 6 +- .../gcp/data/gcp-pub-sub-subscription.json | 2 +- .../sources/gcp/data/gcp-pub-sub-topic.json | 2 +- .../sources/gcp/data/gcp-redis-instance.json | 2 +- .../sources/gcp/data/gcp-run-revision.json | 2 +- .../sources/gcp/data/gcp-run-service.json | 2 +- .../gcp/data/gcp-secret-manager-secret.json | 7 +- ...er-management-security-center-service.json | 6 +- .../data/gcp-service-directory-endpoint.json | 6 +- .../gcp/data/gcp-service-usage-service.json | 7 +- .../gcp/data/gcp-spanner-database.json | 2 +- .../gcp/data/gcp-spanner-instance.json | 6 +- .../gcp/data/gcp-sql-admin-backup-run.json | 2 +- .../gcp/data/gcp-sql-admin-backup.json | 2 +- .../gcp/data/gcp-sql-admin-instance.json | 2 +- .../data/gcp-storage-bucket-iam-policy.json | 2 +- .../sources/gcp/data/gcp-storage-bucket.json | 2 +- .../gcp-storage-transfer-transfer-job.json | 2 +- .../docs/sources/k8s/account_settings.png | Bin 49931 -> 403511 bytes .../docs/sources/k8s/api_key.png | Bin 74421 -> 79291 bytes .../docs/sources/k8s/configuration.md | 6 +- 223 files changed, 726 insertions(+), 693 deletions(-) diff --git a/docs.overmind.tech/docs/sources/aws/account_settings.png b/docs.overmind.tech/docs/sources/aws/account_settings.png index a244a522c60a7a30943e11292545c4fc31820953..c7e184cb1e623fe6890a8390a5efd8cf94945e5d 100644 GIT binary patch literal 403511 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

<_@Raq2O1(n*u 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�PsY`KpTut7T(8@%4?2q zf=|ucct|vWR6TeBr5NbIhxxpR%g>R-4}Oe?IA#2{eI%>Tz*Zie4C^(A7*q0ye(xX% z*~nw8+m4r-)6*wO(2^|yfOW0(25vOePVHR*&4hQw&tuAjNP21hwST1c|6s_4m~2tQ z(VLV2_y-3a-LV5)nCE#ZgdIurEmO=|Dw5wsL`B^(Q@&oQJ%s7e1Bgpe#u4PB7nX=~ z@{tSW-?m%~Uh^!_CFy^MtuC3pYDliG^dCLCCy}Vv#d<@3%l^h~lGjJ~(UzBQ19w*) z)|MaW4r4z=vvBc9t$u;q=@DJ{oK9I8#OBO`tnJzza8g3pTM!cyAJOTvn?s2Orw3`W zDl;AJ(KpkLn_+A;UakiCp=Kr3ARI6PrxhCQ4Jp7-&U)v;SkwzFBL-NROxhfzVNIe@ zMZ1`I{>;N2byz<6&SCp|sjK`9q128;CkDwLz-$<$Em0j2>l*L%LN2C~9=4?mu@QtS zU2vP$@J;hTO{>oD&dZpFM}oF5&w|PE=$A@C^KeUL=f~(xA1KTKp=U8pf>wZiH^_?f ziLfz0I1TEQm?J+K1en&ABw66SG`*1Q4r#jDOld?O zUjJdCMIeyP41@c=AB-Ug^hehpI6J2x$N_hsYLoYXwc$R2(f$@BCB0dgjND+u0nZ;O zYaphxLn#ft7lX5$Awt1}Q@>42*G>VbA*(CQ+$<<2p0Y z5yss2z|J!J1DHKf|A4LyHp9{{SQ+S+cy}70YCm@QBKSsa6-|%Wy-2{_y%60(qF30m zSD20UcDfjdiHj5(l)^Bi`A#9D8Nzb7pya+TC$y0`q!UM{kB$43ic9QrUdh$+(Mvi{ zi`{w$T^m~vpr_NHACgC6JI%njVaXHbAjK7guATUl0%$wNSdoxV8R#e%=OFt)_@JYs zgQn*S;6B_G*}}N$F<4}(`~)_Tc=-7DI<%CdDUj<+U@%qELA8&?mt#T2uF$MWk1X~z z3q{W1ZdZ>6R1M6zwOb|iHcL>wuixugXR2gb-=L9rI#3ZzIgaQy`(@K$+yJqa{3(i0 zeZe<3KJk9O_F-mQuV9Xxzo(*f%p#0W7iN`8=->9s7?hq3p|7aAyF4kJ7~w)fH7#`8 z=z{8t;|oxTFjwRFR_CdxrJs3$&-gQOXe|JJ%ISM5I~jPGBY?a(g9JmJw@hg%P(t)j zT!Ad{N8JDEXElbFU5Kzq`=OJF=!1+rU|(@a@>jG$kR3j?h-R?%`8upP+CIQN9H*1= znZG^!7R>u38?FG_OI#WWQ@nnM#SafM9c%$s&>M3tmjfbzmoWt58L9+rbr|^mWb1L8)DBF&fTdPc+0`M4qhLc|#Tk{RNGI+t}B-7ADo8hzo zxihDr`P(lDe?J}~_4qKzwEx|AQt}K80)5ax(*j`yνFW?;WF>-Nw#vg`B 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

`WHqLU)vx!Yq~nuCsUfe& z^|s5Xx6io(m9iuFo__Hd2u|t^ews;B{U{i9*`Y%&yHK_)7}j1?GvS@SPaL+N^eqWF zG|Thab}Q%56bhfaC4cHddkEtD3;(gCOn0x zjdAxVttu~5wR)nj!{wU4&gP1&$TCjz;nZvo)AW5}P zL(AZU8@aiw&@W~&+=2@4b5U-ojB|Fui*-^yKdNL16Y8?Yl=XkZLLq%l_)OkM_~q%E zjni`XYH?d_!9$+(7{pw&UhaFdSK9CUJ6zZu&?b3AxM7r?^SCt|K zteLowL+oL%Z{L<4+-ERqixw*LY2m1Hmg_*#IbKNrMGexrA#bg}2ji_^%BW`59=M8l+Rk{X<4mE#_44BIzjK^AYEF)9=; ze=)yjJgmXN134!@VXnK;2aJ2h^RKPLB#%$BN|%_&%&y6mkVO<(?$o*uB{$WB;TN9z zN{BNZw+lZz381AElQf(2r;1-La5n>9tx_w`EV@Hcn>el!$$UZ!(8>i7pf};-H(ykT zP$AgoH@^YuVCopL@84lIn+VAh(E`Erc%7I}WSK6s`ZQ_Lj#*vDpzvLO zS|(*9AwY#BCv-P80!D6f&+zpY71+rasE^$=VCXqAXdKTLb<4O0db)K`J}{LCFexXP zdChoN-Hl*Y;?NRL4#D~gGg2VchswNj#_(hGP@%5E-4XTA{PxSej2-8=@l@}V{c#Sz z8215C(l~YvrfaUBA;WBxVyBPj=xwXwegt3EA@t+-b}J<*^!s29pv#j;uYZP3cAr)i zB|EAt8y1|;Eg!c7Y;Q>9vP%0DxI71{H<=cViI8IlPu}x24s~R(q#ADZc3d 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%0ArX!tDIgfi#M4Ea?( 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ⅆ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 literal 45804 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 UaN%#Yb>Qb<=VbfX=1kOo0KsHH^8f$< diff --git a/docs.overmind.tech/docs/sources/aws/aws_source_settings.png b/docs.overmind.tech/docs/sources/aws/aws_source_settings.png index f8f38128e493bc53e18a7d9cebea1858682b3199..b6eca5e4b1cd0f3dae1da1d7ba380b3e0774636c 100644 GIT binary patch literal 127756 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

j_<%-(N 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


COpW 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 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 index 295f0267..a056602b 100644 --- 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 @@ -1 +1,25 @@ -{"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"}]} +{ + "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 index f7f37b4d..90825ea7 100644 --- 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 @@ -1 +1,26 @@ -{"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"}]} +{ + "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 index d0dad238..32375b58 100644 --- 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 @@ -1 +1,22 @@ -{"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"}]} +{ + "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 index 01c80f1b..7936217f 100644 --- 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 @@ -1 +1,26 @@ -{"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"}]} +{ + "type": "ec2-transit-gateway-route", + "category": 3, + "potentialLinks": [ + "ec2-transit-gateway-route-table", + "ec2-transit-gateway-route-table-association", + "ec2-transit-gateway-attachment", + "ec2-transit-gateway-route-table-announcement", + "ec2-vpc", + "ec2-vpn-connection", + "ec2-managed-prefix-list", + "directconnect-direct-connect-gateway" + ], + "descriptiveName": "Transit Gateway Route", + "supportedQueryMethods": { + "get": true, + "getDescription": "Get by TransitGatewayRouteTableId|Destination (CIDR or pl:PrefixListId)", + "list": true, + "listDescription": "List all transit gateway routes", + "search": true, + "searchDescription": "Search by TransitGatewayRouteTableId to list routes for that route table" + }, + "terraformMappings": [ + { "terraformQueryMap": "aws_ec2_transit_gateway_route.id" } + ] +} diff --git a/docs.overmind.tech/docs/sources/aws/terraform.md b/docs.overmind.tech/docs/sources/aws/terraform.md index e976c988..5e780730 100644 --- a/docs.overmind.tech/docs/sources/aws/terraform.md +++ b/docs.overmind.tech/docs/sources/aws/terraform.md @@ -111,19 +111,19 @@ module "overmind_staging" { ## 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 | +| 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 | +| 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 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 index b6cfd8de..ae30c276 100644 --- a/docs.overmind.tech/docs/sources/aws/update-to-pod-identity.md +++ b/docs.overmind.tech/docs/sources/aws/update-to-pod-identity.md @@ -26,11 +26,11 @@ You can check if your IAM role needs updating by looking at the version tag: 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 | +| Version Tag | Status | +| ----------------------- | ------------------ | +| `2025-12-01` or later | ✅ Up to date | | `2023-03-14` or earlier | ⚠️ Update required | -| No tag | ⚠️ Update required | +| No tag | ⚠️ Update required | ## Update Instructions @@ -97,11 +97,11 @@ Search for and select your Overmind role (usually named "Overmind" or the name y ```json { - "Effect": "Allow", - "Principal": { - "AWS": "arn:aws:iam::944651592624:root" - }, - "Action": "sts:TagSession" + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam::944651592624:root" + }, + "Action": "sts:TagSession" } ``` @@ -109,28 +109,28 @@ 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" + "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" + } + ] } ``` diff --git a/docs.overmind.tech/docs/sources/azure/_category_.json b/docs.overmind.tech/docs/sources/azure/_category_.json index 694c142d..ccab81aa 100644 --- a/docs.overmind.tech/docs/sources/azure/_category_.json +++ b/docs.overmind.tech/docs/sources/azure/_category_.json @@ -1 +1,9 @@ -{"label":"Azure","position":4,"collapsed":true,"link":{"type":"generated-index","description":"How to integrate your Azure subscription."}} +{ + "label": "Azure", + "position": 4, + "collapsed": true, + "link": { + "type": "generated-index", + "description": "How to integrate your Azure subscription." + } +} 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 index d3d697b4..fae56aff 100644 --- 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 @@ -8,9 +8,9 @@ Official documentation: https://cloud.google.com/vertex-ai/docs/predictions/batc ## 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' +- `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 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 index 6f833de2..510763fa 100644 --- 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 @@ -8,9 +8,9 @@ Official documentation: https://cloud.google.com/vertex-ai/docs/training/create- ## Supported Methods -* `GET`: Get a gcp-ai-platform-custom-job by its "name" -* `LIST`: List all gcp-ai-platform-custom-job -* ~~`SEARCH`~~ +- `GET`: Get a gcp-ai-platform-custom-job by its "name" +- `LIST`: List all gcp-ai-platform-custom-job +- ~~`SEARCH`~~ ## Possible Links 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 index 3f22d39f..6867c939 100644 --- 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 @@ -8,15 +8,15 @@ Official documentation: https://cloud.google.com/vertex-ai/docs/predictions/gett ## Supported Methods -* `GET`: Get a gcp-ai-platform-endpoint by its "name" -* `LIST`: List all gcp-ai-platform-endpoint -* ~~`SEARCH`~~ +- `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 hosts one or more *DeployedModels*, each of which references a standalone AI Platform/Vertex AI Model resource. The link shows which models are currently deployed to, or have traffic routed through, the endpoint. +An Endpoint hosts one or more _DeployedModels_, each of which references a standalone AI Platform/Vertex AI Model resource. The link shows which models are currently deployed to, or have traffic routed through, the endpoint. ### [`gcp-ai-platform-model-deployment-monitoring-job`](/sources/gcp/Types/gcp-ai-platform-model-deployment-monitoring-job) 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 index b4911e93..a5178ca8 100644 --- 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 @@ -8,9 +8,9 @@ Official documentation: https://cloud.google.com/vertex-ai/docs/model-monitoring ## 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' +- `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 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 index 99e47c18..ad28f59f 100644 --- 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 @@ -7,9 +7,9 @@ A GCP AI Platform Model (now part of Vertex AI) is a logical container that hold ## Supported Methods -* `GET`: Get a gcp-ai-platform-model by its "name" -* `LIST`: List all gcp-ai-platform-model -* ~~`SEARCH`~~ +- `GET`: Get a gcp-ai-platform-model by its "name" +- `LIST`: List all gcp-ai-platform-model +- ~~`SEARCH`~~ ## Possible Links 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 index 9d77b26d..23aab5ae 100644 --- 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 @@ -8,9 +8,9 @@ Official documentation: https://cloud.google.com/vertex-ai/docs/pipelines/introd ## Supported Methods -* `GET`: Get a gcp-ai-platform-pipeline-job by its "name" -* `LIST`: List all gcp-ai-platform-pipeline-job -* ~~`SEARCH`~~ +- `GET`: Get a gcp-ai-platform-pipeline-job by its "name" +- `LIST`: List all gcp-ai-platform-pipeline-job +- ~~`SEARCH`~~ ## Possible Links 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 index 7bdda6ec..b346f80f 100644 --- 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 @@ -8,10 +8,10 @@ For more information, see the official documentation: https://cloud.google.com/a **Terrafrom Mappings:** -* `google_artifact_registry_docker_image.name` +- `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. +- `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 index 3092a3ed..c7dc4bae 100644 --- 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 @@ -8,13 +8,13 @@ For a full description of the resource see the Google Cloud documentation: https **Terrafrom Mappings:** -* `google_bigquery_data_transfer_config.id` +- `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. +- `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 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 index 3f6de581..41435289 100644 --- 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 @@ -7,16 +7,16 @@ A Google Cloud BigQuery Dataset is a logical container that holds tables, views, **Terrafrom Mappings:** -* `google_bigquery_dataset.dataset_id` -* `google_bigquery_dataset_iam_binding.dataset_id` -* `google_bigquery_dataset_iam_member.dataset_id` -* `google_bigquery_dataset_iam_policy.dataset_id` +- `google_bigquery_dataset.dataset_id` +- `google_bigquery_dataset_iam_binding.dataset_id` +- `google_bigquery_dataset_iam_member.dataset_id` +- `google_bigquery_dataset_iam_policy.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`~~ +- `GET`: Get GCP Big Query Dataset by "gcp-big-query-dataset-id" +- `LIST`: List all GCP Big Query Dataset items +- ~~`SEARCH`~~ ## Possible Links 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 index ed6afa3a..62ddb34a 100644 --- 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 @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/bigquery/docs/reference/rest/v2 **Terrafrom Mappings:** -* `google_bigquery_routine.id` +- `google_bigquery_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" +- `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 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 index 162c3bd5..5b9487fc 100644 --- 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 @@ -8,16 +8,16 @@ Official documentation: https://cloud.google.com/bigquery/docs/tables **Terrafrom Mappings:** -* `google_bigquery_table.id` -* `google_bigquery_table_iam_binding.dataset_id` -* `google_bigquery_table_iam_member.dataset_id` -* `google_bigquery_table_iam_policy.dataset_id` +- `google_bigquery_table.id` +- `google_bigquery_table_iam_binding.dataset_id` +- `google_bigquery_table_iam_member.dataset_id` +- `google_bigquery_table_iam_policy.dataset_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" +- `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 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 index 2de4ace1..ab874cf5 100644 --- 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 @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/bigtable/docs/app-profiles **Terrafrom Mappings:** -* `google_bigtable_app_profile.id` +- `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. +- `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 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 index 8c212548..b7cdf6a7 100644 --- 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 @@ -8,9 +8,9 @@ Official documentation: https://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" +- `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 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 index 3bab2c7d..8160b874 100644 --- 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 @@ -8,9 +8,9 @@ For further details, see Google’s official documentation: https://cloud.google ## 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" +- `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 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 index 64af5caa..6b010f4f 100644 --- 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 @@ -7,16 +7,16 @@ Cloud Bigtable instances are the top-level administrative containers for all tab **Terrafrom Mappings:** -* `google_bigtable_instance.name` -* `google_bigtable_instance_iam_binding.instance` -* `google_bigtable_instance_iam_member.instance` -* `google_bigtable_instance_iam_policy.instance` +- `google_bigtable_instance.name` +- `google_bigtable_instance_iam_binding.instance` +- `google_bigtable_instance_iam_member.instance` +- `google_bigtable_instance_iam_policy.instance` ## Supported Methods -* `GET`: Get a gcp-big-table-admin-instance by its "name" -* `LIST`: List all gcp-big-table-admin-instance -* ~~`SEARCH`~~ +- `GET`: Get a gcp-big-table-admin-instance by its "name" +- `LIST`: List all gcp-big-table-admin-instance +- ~~`SEARCH`~~ ## Possible Links 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 index 908e5ba7..62f6a263 100644 --- 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 @@ -7,16 +7,16 @@ Google Cloud Bigtable is a scalable NoSQL database service for large analytical **Terrafrom Mappings:** -* `google_bigtable_table.id` -* `google_bigtable_table_iam_binding.instance_name` -* `google_bigtable_table_iam_member.instance_name` -* `google_bigtable_table_iam_policy.instance_name` +- `google_bigtable_table.id` +- `google_bigtable_table_iam_binding.instance_name` +- `google_bigtable_table_iam_member.instance_name` +- `google_bigtable_table_iam_policy.instance_name` ## 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. +- `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 diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-certificate-manager-certificate.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-certificate-manager-certificate.md index b957b9e4..3623be9f 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-certificate-manager-certificate.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-certificate-manager-certificate.md @@ -7,10 +7,10 @@ A **GCP Certificate Manager Certificate** represents an SSL/TLS certificate that **Terrafrom Mappings:** -* `google_certificate_manager_certificate.id` +- `google_certificate_manager_certificate.id` ## Supported Methods -* `GET`: Get GCP Certificate Manager Certificate by "gcp-certificate-manager-certificate-location|gcp-certificate-manager-certificate-name" -* ~~`LIST`~~ -* `SEARCH`: Search for GCP Certificate Manager Certificate by "gcp-certificate-manager-certificate-location" +- `GET`: Get GCP Certificate Manager Certificate by "gcp-certificate-manager-certificate-location|gcp-certificate-manager-certificate-name" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Certificate Manager Certificate by "gcp-certificate-manager-certificate-location" 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 index 62dd35ca..08b81211 100644 --- 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 @@ -14,9 +14,9 @@ Knowing the contents of this object allows Overmind to determine, for example, w ## Supported Methods -* `GET`: Get a gcp-cloud-billing-billing-info by its "name" -* ~~`LIST`~~ -* ~~`SEARCH`~~ +- `GET`: Get a gcp-cloud-billing-billing-info by its "name" +- ~~`LIST`~~ +- ~~`SEARCH`~~ ## Possible Links 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 index 435d75a0..3ff463e4 100644 --- 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 @@ -8,9 +8,9 @@ See the official documentation for full details: https://cloud.google.com/build/ ## Supported Methods -* `GET`: Get a gcp-cloud-build-build by its "name" -* `LIST`: List all gcp-cloud-build-build -* ~~`SEARCH`~~ +- `GET`: Get a gcp-cloud-build-build by its "name" +- `LIST`: List all gcp-cloud-build-build +- ~~`SEARCH`~~ ## Possible Links 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 index 2100db24..07856620 100644 --- 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 @@ -7,9 +7,9 @@ Google Cloud Functions is a server-less execution environment that lets you run ## Supported Methods -* `GET`: Get a gcp-cloud-functions-function by its "locations|functions" -* ~~`LIST`~~ -* `SEARCH`: Search for gcp-cloud-functions-function by its "locations" +- `GET`: Get a gcp-cloud-functions-function by its "locations|functions" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-cloud-functions-function by its "locations" ## Possible Links 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 index ac86612c..ef5e8169 100644 --- 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 @@ -7,13 +7,13 @@ A **Cloud KMS CryptoKeyVersion** is an immutable representation of a single piec **Terrafrom Mappings:** -* `google_kms_crypto_key_version.id` +- `google_kms_crypto_key_version.id` ## Supported Methods -* `GET`: Get GCP Cloud Kms Crypto Key Version by "gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name|gcp-cloud-kms-crypto-key-name|gcp-cloud-kms-crypto-key-version-version" -* ~~`LIST`~~ -* `SEARCH`: Search for GCP Cloud Kms Crypto Key Version by "gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name|gcp-cloud-kms-crypto-key-name" +- `GET`: Get GCP Cloud Kms Crypto Key Version by "gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name|gcp-cloud-kms-crypto-key-name|gcp-cloud-kms-crypto-key-version-version" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Cloud Kms Crypto Key Version by "gcp-cloud-kms-key-ring-location|gcp-cloud-kms-key-ring-name|gcp-cloud-kms-crypto-key-name" ## Possible Links 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 index 688b48fd..25af555c 100644 --- 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 @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/kms/docs/object-hierarchy#key **Terrafrom Mappings:** -* `google_kms_crypto_key.id` +- `google_kms_crypto_key.id` ## 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" +- `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 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 index 7a7e08c9..9893a1d2 100644 --- 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 @@ -8,13 +8,13 @@ For full details, see the official documentation: https://cloud.google.com/kms/d **Terrafrom Mappings:** -* `google_kms_key_ring.id` +- `google_kms_key_ring.id` ## 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 Ring items -* `SEARCH`: Search for GCP Cloud Kms Key Ring by "gcp-cloud-kms-key-ring-location" +- `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 Ring items +- `SEARCH`: Search for GCP Cloud Kms Key Ring by "gcp-cloud-kms-key-ring-location" ## Possible Links 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 index b0f61071..492a056e 100644 --- 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 @@ -3,11 +3,11 @@ title: GCP Cloud Resource Manager Project sidebar_label: gcp-cloud-resource-manager-project --- -A Google Cloud Resource Manager Project represents the fundamental organisational unit within Google Cloud Platform (GCP). Every compute, storage or networking asset you create must live inside a Project, which in turn sits under a Folder or Organisation node. Projects provide isolated boundaries for Identity and Access Management (IAM), quotas, billing, API enablement and lifecycle operations such as creation, update, suspension and deletion. By modelling Projects, Overmind can surface risks linked to mis-scoped IAM roles, neglected billing settings or interactions with other resources *before* any change is pushed to production. +A Google Cloud Resource Manager Project represents the fundamental organisational unit within Google Cloud Platform (GCP). Every compute, storage or networking asset you create must live inside a Project, which in turn sits under a Folder or Organisation node. Projects provide isolated boundaries for Identity and Access Management (IAM), quotas, billing, API enablement and lifecycle operations such as creation, update, suspension and deletion. By modelling Projects, Overmind can surface risks linked to mis-scoped IAM roles, neglected billing settings or interactions with other resources _before_ any change is pushed to production. Official 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`~~ +- `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 index 7f38521a..2d62d471 100644 --- 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 @@ -8,10 +8,10 @@ For a full description of Tag Values and how they fit into the tagging system, r **Terrafrom Mappings:** -* `google_tags_tag_value.name` +- `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. +- `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 index 6dd0eb5d..bc91e016 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-address.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-address.md @@ -7,13 +7,13 @@ A GCP Compute Address is a reserved, static IP address that can be either region **Terrafrom Mappings:** -* `google_compute_address.name` +- `google_compute_address.name` ## Supported Methods -* `GET`: Get GCP Compute Address by "gcp-compute-address-name" -* `LIST`: List all GCP Compute Address items -* ~~`SEARCH`~~ +- `GET`: Get GCP Compute Address by "gcp-compute-address-name" +- `LIST`: List all GCP Compute Address items +- ~~`SEARCH`~~ ## Possible Links 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 index ac697652..2ee81ac6 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-autoscaler.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-autoscaler.md @@ -7,13 +7,13 @@ A GCP Compute Autoscaler is a zonal or regional resource that automatically adds **Terrafrom Mappings:** -* `google_compute_autoscaler.name` +- `google_compute_autoscaler.name` ## Supported Methods -* `GET`: Get GCP Compute Autoscaler by "gcp-compute-autoscaler-name" -* `LIST`: List all GCP Compute Autoscaler items -* ~~`SEARCH`~~ +- `GET`: Get GCP Compute Autoscaler by "gcp-compute-autoscaler-name" +- `LIST`: List all GCP Compute Autoscaler items +- ~~`SEARCH`~~ ## Possible Links 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 index 035e8226..b29c676f 100644 --- 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 @@ -8,14 +8,14 @@ For full details see the official Google Cloud documentation: https://cloud.goog **Terrafrom Mappings:** -* `google_compute_backend_service.name` -* `google_compute_region_backend_service.name` +- `google_compute_backend_service.name` +- `google_compute_region_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`~~ +- `GET`: Get GCP Compute Backend Service by "gcp-compute-backend-service-name" +- `LIST`: List all GCP Compute Backend Service items +- ~~`SEARCH`~~ ## Possible Links 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 index de49b064..4d61d32c 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-disk.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-disk.md @@ -7,13 +7,13 @@ A GCP Compute Disk—formally known as a Persistent Disk—is block-level storag **Terrafrom Mappings:** -* `google_compute_disk.name` +- `google_compute_disk.name` ## Supported Methods -* `GET`: Get GCP Compute Disk by "gcp-compute-disk-name" -* `LIST`: List all GCP Compute Disk items -* ~~`SEARCH`~~ +- `GET`: Get GCP Compute Disk by "gcp-compute-disk-name" +- `LIST`: List all GCP Compute Disk items +- ~~`SEARCH`~~ ## Possible Links 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 index 38a9e375..353fc407 100644 --- 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 @@ -8,10 +8,10 @@ For further details, see the official documentation: https://cloud.google.com/co **Terrafrom Mappings:** -* `google_compute_external_vpn_gateway.name` +- `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`~~ +- `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 index 747162c1..e970ca4b 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-firewall.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-firewall.md @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/vpc/docs/firewalls **Terrafrom Mappings:** -* `google_compute_firewall.name` +- `google_compute_firewall.name` ## Supported Methods -* `GET`: Get a gcp-compute-firewall by its "name" -* `LIST`: List all gcp-compute-firewall -* `SEARCH`: Search for firewalls by network tag. The query is a plain network tag name. +- `GET`: Get a gcp-compute-firewall by its "name" +- `LIST`: List all gcp-compute-firewall +- `SEARCH`: Search for firewalls by network tag. The query is a plain network tag name. ## Possible Links 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 index e3f6f983..39d030b3 100644 --- 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 @@ -8,13 +8,13 @@ For full details see the official documentation: https://cloud.google.com/load-b **Terrafrom Mappings:** -* `google_compute_forwarding_rule.name` +- `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`~~ +- `GET`: Get GCP Compute Forwarding Rule by "gcp-compute-forwarding-rule-name" +- `LIST`: List all GCP Compute Forwarding Rule items +- ~~`SEARCH`~~ ## Possible Links 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 index ce177827..ffbe9cb8 100644 --- 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 @@ -3,18 +3,18 @@ title: GCP Compute Global Address sidebar_label: gcp-compute-global-address --- -A **Compute Global Address** in Google Cloud Platform is a statically-reserved IP address that is reachable from, or usable across, all regions. It can be external (used, for example, by a global HTTP(S) load balancer) or internal (used by regional resources that require a routable, private global IP). Reserving the address ensures it does not change while it is in use, and allows it to be assigned to resources at creation time or later. +A **Compute Global Address** in Google Cloud Platform is a statically-reserved IP address that is reachable from, or usable across, all regions. It can be external (used, for example, by a global HTTP(S) load balancer) or internal (used by regional resources that require a routable, private global IP). Reserving the address ensures it does not change while it is in use, and allows it to be assigned to resources at creation time or later. Official documentation: https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address **Terrafrom Mappings:** -* `google_compute_global_address.name` +- `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`~~ +- `GET`: Get a gcp-compute-global-address by its "name" +- `LIST`: List all gcp-compute-global-address +- ~~`SEARCH`~~ ## Possible Links 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 index e442cdcb..4f56abf5 100644 --- 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 @@ -7,13 +7,13 @@ A Google Cloud Compute Global Forwarding Rule defines a single anycast virtual I **Terrafrom Mappings:** -* `google_compute_global_forwarding_rule.name` +- `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`~~ +- `GET`: Get a gcp-compute-global-forwarding-rule by its "name" +- `LIST`: List all gcp-compute-global-forwarding-rule +- ~~`SEARCH`~~ ## Possible Links 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 index 6f503707..b94f59de 100644 --- 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 @@ -8,11 +8,11 @@ Official documentation: https://cloud.google.com/load-balancing/docs/health-chec **Terrafrom Mappings:** -* `google_compute_health_check.name` -* `google_compute_region_health_check.name` +- `google_compute_health_check.name` +- `google_compute_region_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`~~ +- `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 index ff27f4ef..987b860c 100644 --- 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 @@ -8,10 +8,10 @@ For further details see the official documentation: https://cloud.google.com/com **Terrafrom Mappings:** -* `google_compute_http_health_check.name` +- `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`~~ +- `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 index 525f34d3..9ccc9158 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-image.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-image.md @@ -8,13 +8,13 @@ For full details see the official documentation: https://cloud.google.com/comput **Terrafrom Mappings:** -* `google_compute_image.name` +- `google_compute_image.name` ## Supported Methods -* `GET`: Get GCP Compute Image by "gcp-compute-image-name" -* `LIST`: List all GCP Compute Image items -* `SEARCH`: Search for GCP Compute Image by "gcp-compute-image-family" +- `GET`: Get GCP Compute Image by "gcp-compute-image-name" +- `LIST`: List all GCP Compute Image items +- `SEARCH`: Search for GCP Compute Image by "gcp-compute-image-family" ## Possible Links 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 index 81ad76be..c9e3bf24 100644 --- 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 @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/compute/docs/instance-groups/cr **Terrafrom Mappings:** -* `google_compute_instance_group_manager.name` +- `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`~~ +- `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 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 index 6f59c0ad..5b45fd96 100644 --- 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 @@ -8,13 +8,13 @@ For full details see the official Google Cloud documentation: https://cloud.goog **Terrafrom Mappings:** -* `google_compute_instance_group.name` +- `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`~~ +- `GET`: Get GCP Compute Instance Group by "gcp-compute-instance-group-name" +- `LIST`: List all GCP Compute Instance Group items +- ~~`SEARCH`~~ ## Possible Links 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 index 294e9dc2..a109bd06 100644 --- 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 @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/compute/docs/instance-templates **Terrafrom Mappings:** -* `google_compute_instance_template.name` +- `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`: Search for instance templates by network tag. The query is a plain network tag name. +- `GET`: Get a gcp-compute-instance-template by its "name" +- `LIST`: List all gcp-compute-instance-template +- `SEARCH`: Search for instance templates by network tag. The query is a plain network tag name. ## Possible Links 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 index f39ff54d..a945f36a 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-instance.md @@ -7,13 +7,13 @@ A Google Cloud Compute Engine instance is a virtual machine (VM) that runs on Go **Terrafrom Mappings:** -* `google_compute_instance.name` +- `google_compute_instance.name` ## Supported Methods -* `GET`: Get GCP Compute Instance by "gcp-compute-instance-name" -* `LIST`: List all GCP Compute Instance items -* `SEARCH`: Search for GCP Compute Instance by "gcp-compute-instance-networkTag" +- `GET`: Get GCP Compute Instance by "gcp-compute-instance-name" +- `LIST`: List all GCP Compute Instance items +- `SEARCH`: Search for GCP Compute Instance by "gcp-compute-instance-networkTag" ## Possible Links 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 index ffd90f5d..82699609 100644 --- 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 @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/compute/docs/disks/instant-snap **Terrafrom Mappings:** -* `google_compute_instant_snapshot.name` +- `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`~~ +- `GET`: Get GCP Compute Instant Snapshot by "gcp-compute-instant-snapshot-name" +- `LIST`: List all GCP Compute Instant Snapshot items +- ~~`SEARCH`~~ ## Possible Links 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 index 899ebb1e..5cb79bfb 100644 --- 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 @@ -7,13 +7,13 @@ A Google Cloud Compute Machine Image is a first-class resource that captures the **Terrafrom Mappings:** -* `google_compute_machine_image.name` +- `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`~~ +- `GET`: Get GCP Compute Machine Image by "gcp-compute-machine-image-name" +- `LIST`: List all GCP Compute Machine Image items +- ~~`SEARCH`~~ ## Possible Links 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 index 42dcdecd..818ed55f 100644 --- 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 @@ -7,13 +7,13 @@ A Google Cloud Platform Compute Network Endpoint Group (NEG) is a collection of **Terrafrom Mappings:** -* `google_compute_network_endpoint_group.name` +- `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`~~ +- `GET`: Get a gcp-compute-network-endpoint-group by its "name" +- `LIST`: List all gcp-compute-network-endpoint-group +- ~~`SEARCH`~~ ## Possible Links 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 index 0ef3dcf8..64c63197 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-network.md @@ -7,13 +7,13 @@ A Google Cloud Platform (GCP) Compute Network—commonly called a Virtual Privat **Terrafrom Mappings:** -* `google_compute_network.name` +- `google_compute_network.name` ## Supported Methods -* `GET`: Get a gcp-compute-network by its "name" -* `LIST`: List all gcp-compute-network -* ~~`SEARCH`~~ +- `GET`: Get a gcp-compute-network by its "name" +- `LIST`: List all gcp-compute-network +- ~~`SEARCH`~~ ## Possible Links 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 index 38ab8552..68faa42b 100644 --- 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 @@ -7,11 +7,11 @@ A GCP Compute Node Group is a managed collection of sole-tenant nodes that are a **Terrafrom Mappings:** -* `google_compute_node_group.name` -* `google_compute_node_template.name` +- `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" +- `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-node-template.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-template.md index 66b49159..b44b560a 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-template.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-node-template.md @@ -8,13 +8,13 @@ For a full specification of the resource, see the official Google Cloud document **Terrafrom Mappings:** -* `google_compute_node_template.name` +- `google_compute_node_template.name` ## Supported Methods -* `GET`: Get GCP Compute Node Template by "gcp-compute-node-template-name" -* `LIST`: List all GCP Compute Node Template items -* ~~`SEARCH`~~ +- `GET`: Get GCP Compute Node Template by "gcp-compute-node-template-name" +- `LIST`: List all GCP Compute Node Template items +- ~~`SEARCH`~~ ## Possible Links 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 index 374f8b46..776fa9dd 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-project.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-project.md @@ -7,20 +7,20 @@ A Google Cloud Project is the fundamental organisational unit in Google Cloud Pl **Terrafrom Mappings:** -* `google_project.project_id` -* `google_compute_shared_vpc_host_project.project` -* `google_compute_shared_vpc_service_project.service_project` -* `google_compute_shared_vpc_service_project.host_project` -* `google_project_iam_binding.project` -* `google_project_iam_member.project` -* `google_project_iam_policy.project` -* `google_project_iam_audit_config.project` +- `google_project.project_id` +- `google_compute_shared_vpc_host_project.project` +- `google_compute_shared_vpc_service_project.service_project` +- `google_compute_shared_vpc_service_project.host_project` +- `google_project_iam_binding.project` +- `google_project_iam_member.project` +- `google_project_iam_policy.project` +- `google_project_iam_audit_config.project` ## Supported Methods -* `GET`: Get a gcp-compute-project by its "name" -* ~~`LIST`~~ -* ~~`SEARCH`~~ +- `GET`: Get a gcp-compute-project by its "name" +- ~~`LIST`~~ +- ~~`SEARCH`~~ ## Possible Links 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 index 25990faf..79c4a254 100644 --- 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 @@ -8,13 +8,13 @@ For full details, see the official documentation: https://cloud.google.com/vpc/d **Terrafrom Mappings:** -* `google_compute_public_delegated_prefix.id` +- `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). +- `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 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 index cc43218c..385724cc 100644 --- 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 @@ -7,13 +7,13 @@ A Compute Region Commitment in Google Cloud Platform (GCP) represents a contract **Terrafrom Mappings:** -* `google_compute_region_commitment.name` +- `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`~~ +- `GET`: Get a gcp-compute-region-commitment by its "name" +- `LIST`: List all gcp-compute-region-commitment +- ~~`SEARCH`~~ ## Possible Links diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-regional-instance-group-manager.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-regional-instance-group-manager.md index e66363c7..f291a1ce 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-regional-instance-group-manager.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-regional-instance-group-manager.md @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/compute/docs/instance-groups/cr **Terrafrom Mappings:** -* `google_compute_region_instance_group_manager.name` +- `google_compute_region_instance_group_manager.name` ## Supported Methods -* `GET`: Get GCP Compute Regional Instance Group Manager by "gcp-compute-regional-instance-group-manager-name" -* `LIST`: List all GCP Compute Regional Instance Group Manager items -* ~~`SEARCH`~~ +- `GET`: Get GCP Compute Regional Instance Group Manager by "gcp-compute-regional-instance-group-manager-name" +- `LIST`: List all GCP Compute Regional Instance Group Manager items +- ~~`SEARCH`~~ ## Possible Links 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 index 7255f463..1fe5fe3e 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-reservation.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-reservation.md @@ -7,13 +7,13 @@ A GCP Compute Reservation is a zonal capacity-planning resource that lets you pr **Terrafrom Mappings:** -* `google_compute_reservation.name` +- `google_compute_reservation.name` ## Supported Methods -* `GET`: Get GCP Compute Reservation by "gcp-compute-reservation-name" -* `LIST`: List all GCP Compute Reservation items -* ~~`SEARCH`~~ +- `GET`: Get GCP Compute Reservation by "gcp-compute-reservation-name" +- `LIST`: List all GCP Compute Reservation items +- ~~`SEARCH`~~ ## Possible Links 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 index f26ad359..e4031983 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-route.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-route.md @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/vpc/docs/routes **Terrafrom Mappings:** -* `google_compute_route.name` +- `google_compute_route.name` ## Supported Methods -* `GET`: Get a gcp-compute-route by its "name" -* `LIST`: List all gcp-compute-route -* `SEARCH`: Search for routes by network tag. The query is a plain network tag name. +- `GET`: Get a gcp-compute-route by its "name" +- `LIST`: List all gcp-compute-route +- `SEARCH`: Search for routes by network tag. The query is a plain network tag name. ## Possible Links 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 index 751d8844..fcb42c21 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-router.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-router.md @@ -7,13 +7,13 @@ A Google Cloud Compute Router is a fully distributed and managed Border Gateway **Terrafrom Mappings:** -* `google_compute_router.id` +- `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). +- `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 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 index 1cfcb57c..60cb8cec 100644 --- 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 @@ -8,10 +8,10 @@ For full details, see the official Google documentation: https://cloud.google.co **Terrafrom Mappings:** -* `google_compute_security_policy.name` +- `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`~~ +- `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 index 4acb029a..bc72cc89 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-snapshot.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-snapshot.md @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/compute/docs/disks/create-snaps **Terrafrom Mappings:** -* `google_compute_snapshot.name` +- `google_compute_snapshot.name` ## Supported Methods -* `GET`: Get GCP Compute Snapshot by "gcp-compute-snapshot-name" -* `LIST`: List all GCP Compute Snapshot items -* ~~`SEARCH`~~ +- `GET`: Get GCP Compute Snapshot by "gcp-compute-snapshot-name" +- `LIST`: List all GCP Compute Snapshot items +- ~~`SEARCH`~~ ## Possible Links 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 index bbe0edb6..bcdd663c 100644 --- 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 @@ -7,10 +7,10 @@ A **Google Compute SSL Certificate** represents an SSL certificate resource that **Terrafrom Mappings:** -* `google_compute_ssl_certificate.name` +- `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`~~ +- `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 index de1bc455..f0b5c098 100644 --- 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 @@ -8,10 +8,10 @@ For detailed information, refer to the official Google Cloud documentation: http **Terrafrom Mappings:** -* `google_compute_ssl_policy.name` +- `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`~~ +- `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 index 8065aec1..229d73bf 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-subnetwork.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-compute-subnetwork.md @@ -7,13 +7,13 @@ A GCP Compute Subnetwork is a regional, layer-3 virtual network segment that bel **Terrafrom Mappings:** -* `google_compute_subnetwork.name` +- `google_compute_subnetwork.name` ## Supported Methods -* `GET`: Get a gcp-compute-subnetwork by its "name" -* `LIST`: List all gcp-compute-subnetwork -* ~~`SEARCH`~~ +- `GET`: Get a gcp-compute-subnetwork by its "name" +- `LIST`: List all gcp-compute-subnetwork +- ~~`SEARCH`~~ ## Possible Links 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 index 0b7c57e6..612ebcb2 100644 --- 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 @@ -8,13 +8,13 @@ See the official documentation for full details: https://cloud.google.com/load-b **Terrafrom Mappings:** -* `google_compute_target_http_proxy.name` +- `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`~~ +- `GET`: Get a gcp-compute-target-http-proxy by its "name" +- `LIST`: List all gcp-compute-target-http-proxy +- ~~`SEARCH`~~ ## Possible Links 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 index 5ad1026b..09ffa4b7 100644 --- 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 @@ -8,13 +8,13 @@ For full details see the official documentation: https://cloud.google.com/comput **Terrafrom Mappings:** -* `google_compute_target_https_proxy.name` +- `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`~~ +- `GET`: Get a gcp-compute-target-https-proxy by its "name" +- `LIST`: List all gcp-compute-target-https-proxy +- ~~`SEARCH`~~ ## Possible Links 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 index babd509f..1ee2983a 100644 --- 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 @@ -7,13 +7,13 @@ A Google Cloud Compute Target Pool is a regional grouping of VM instances that a **Terrafrom Mappings:** -* `google_compute_target_pool.id` +- `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). +- `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 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 index bd8b873f..339a1581 100644 --- 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 @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/load-balancing/docs/url-map-con **Terrafrom Mappings:** -* `google_compute_url_map.name` +- `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`~~ +- `GET`: Get a gcp-compute-url-map by its "name" +- `LIST`: List all gcp-compute-url-map +- ~~`SEARCH`~~ ## Possible Links 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 index 26c75780..9e892355 100644 --- 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 @@ -8,13 +8,13 @@ See the official Google Cloud documentation for full details: https://cloud.goog **Terrafrom Mappings:** -* `google_compute_ha_vpn_gateway.name` +- `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`~~ +- `GET`: Get a gcp-compute-vpn-gateway by its "name" +- `LIST`: List all gcp-compute-vpn-gateway +- ~~`SEARCH`~~ ## Possible Links 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 index 00eb2786..af31b75c 100644 --- 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 @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/compute/docs/reference/rest/v1/ **Terrafrom Mappings:** -* `google_compute_vpn_tunnel.name` +- `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`~~ +- `GET`: Get a gcp-compute-vpn-tunnel by its "name" +- `LIST`: List all gcp-compute-vpn-tunnel +- ~~`SEARCH`~~ ## Possible Links 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 index 3292ae54..155018b9 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-cluster.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-container-cluster.md @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/kubernetes-engine/docs/concepts **Terrafrom Mappings:** -* `google_container_cluster.id` +- `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. +- `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 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 index e2ad3b5b..a0190d92 100644 --- 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 @@ -3,19 +3,19 @@ title: GCP Container Node Pool sidebar_label: gcp-container-node-pool --- -Google Kubernetes Engine (GKE) runs worker nodes in groups called *node pools*. +Google Kubernetes Engine (GKE) runs worker nodes in groups called _node pools_. Each pool defines the machine type, disk configuration, Kubernetes version and other attributes for the virtual machines that will back your workloads, and can be scaled or upgraded independently from the rest of the cluster. Official documentation: https://cloud.google.com/kubernetes-engine/docs/concepts/node-pools **Terrafrom Mappings:** -* `google_container_node_pool.id` +- `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]" +- `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 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 index ba4e7380..b4d0c48b 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataform-repository.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataform-repository.md @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/dataform/reference/rest **Terrafrom Mappings:** -* `google_dataform_repository.id` +- `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. +- `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 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 index 33a59939..5261705d 100644 --- 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 @@ -8,10 +8,10 @@ For further details see the official API reference: https://cloud.google.com/dat **Terrafrom Mappings:** -* `google_dataplex_aspect_type.id` +- `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. +- `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 index 036a3d1a..dbcd9070 100644 --- 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 @@ -8,13 +8,13 @@ For full details see the official REST reference: https://cloud.google.com/datap **Terrafrom Mappings:** -* `google_dataplex_datascan.id` +- `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. +- `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 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 index 8c0ff895..ffcc1c6b 100644 --- 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 @@ -8,10 +8,10 @@ Official documentation: https://cloud.google.com/data-catalog/docs/reference/res **Terrafrom Mappings:** -* `google_dataplex_entry_group.id` +- `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. +- `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 index fa25c6b7..f897865f 100644 --- 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 @@ -8,10 +8,10 @@ For a full description of each field and the underlying API, see the official Go **Terraform Mappings:** -* `google_dataproc_autoscaling_policy.name` +- `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`~~ +- `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 index c78f2bf8..82fba59e 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-cluster.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-dataproc-cluster.md @@ -7,13 +7,13 @@ A Google Cloud Dataproc Cluster is a managed group of Compute Engine virtual mac **Terrafrom Mappings:** -* `google_dataproc_cluster.name` +- `google_dataproc_cluster.name` ## Supported Methods -* `GET`: Get a gcp-dataproc-cluster by its "name" -* `LIST`: List all gcp-dataproc-cluster -* ~~`SEARCH`~~ +- `GET`: Get a gcp-dataproc-cluster by its "name" +- `LIST`: List all gcp-dataproc-cluster +- ~~`SEARCH`~~ ## Possible Links 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 index fa9ffaa9..f3a4f130 100644 --- 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 @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/dns/docs/zones **Terrafrom Mappings:** -* `google_dns_managed_zone.name` +- `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`~~ +- `GET`: Get a gcp-dns-managed-zone by its "name" +- `LIST`: List all gcp-dns-managed-zone +- ~~`SEARCH`~~ ## Possible Links 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 index 43b35590..96d94d99 100644 --- 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 @@ -8,10 +8,10 @@ For further details, refer to the official Google Cloud documentation: https://c **Terrafrom Mappings:** -* `google_essential_contacts_contact.id` +- `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]". +- `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 index ba63d761..4f4693ee 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-file-instance.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-file-instance.md @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/filestore/docs/overview **Terrafrom Mappings:** -* `google_filestore_instance.id` +- `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. +- `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 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 index 7f183ee0..f4ffc0ed 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-role.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-iam-role.md @@ -7,6 +7,6 @@ A **Google Cloud IAM Role** is a logical grouping of one or more IAM permissions ## Supported Methods -* `GET`: Get a gcp-iam-role by its "name" -* `LIST`: List all gcp-iam-role -* ~~`SEARCH`~~ +- `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 index c8da0dd7..a2e0d84e 100644 --- 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 @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/iam/docs/creating-managing-serv **Terrafrom Mappings:** -* `google_service_account_key.id` +- `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" +- `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 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 index aada9443..2fa7bf68 100644 --- 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 @@ -8,14 +8,14 @@ For full details see the official documentation: https://cloud.google.com/iam/do **Terrafrom Mappings:** -* `google_service_account.email` -* `google_service_account.unique_id` +- `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`~~ +- `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 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 index f7b3702d..8b2a3d9b 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-bucket.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-bucket.md @@ -8,9 +8,9 @@ See the official documentation for full details: https://cloud.google.com/loggin ## Supported Methods -* `GET`: Get a gcp-logging-bucket by its "locations|buckets" -* ~~`LIST`~~ -* `SEARCH`: Search for gcp-logging-bucket by its "locations" +- `GET`: Get a gcp-logging-bucket by its "locations|buckets" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-logging-bucket by its "locations" ## Possible Links 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 index 0b3acdc9..9ecd6a83 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-link.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-link.md @@ -3,18 +3,18 @@ title: GCP Logging Link sidebar_label: gcp-logging-link --- -A GCP Logging Link is a Cloud Logging resource that continuously streams the log entries stored in a specific Log Bucket into an external BigQuery dataset. By configuring a link you enable near-real-time analytics of your logs with BigQuery without the need for manual exports or scheduled jobs. Links are created under the path +A GCP Logging Link is a Cloud Logging resource that continuously streams the log entries stored in a specific Log Bucket into an external BigQuery dataset. By configuring a link you enable near-real-time analytics of your logs with BigQuery without the need for manual exports or scheduled jobs. Links are created under the path -`projects|folders|organizations|billingAccounts / locations / buckets / links` +`projects|folders|organizations|billingAccounts / locations / buckets / links` and each link specifies the destination BigQuery dataset, IAM writer identity, and lifecycle state. For further details see Google’s 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" +- `GET`: Get a gcp-logging-link by its "locations|buckets|links" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-logging-link by its "locations|buckets" ## Possible Links 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 index 4176d787..7802591d 100644 --- 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 @@ -8,6 +8,6 @@ Official documentation: https://cloud.google.com/logging/docs/view/building-quer ## Supported Methods -* `GET`: Get a gcp-logging-saved-query by its "locations|savedQueries" -* ~~`LIST`~~ -* `SEARCH`: Search for gcp-logging-saved-query by its "locations" +- `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 index e120fc1e..e6bf2157 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-sink.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-logging-sink.md @@ -8,9 +8,9 @@ Official documentation: https://cloud.google.com/logging/docs/export ## Supported Methods -* `GET`: Get GCP Logging Sink by "gcp-logging-sink-name" -* `LIST`: List all GCP Logging Sink items -* ~~`SEARCH`~~ +- `GET`: Get GCP Logging Sink by "gcp-logging-sink-name" +- `LIST`: List all GCP Logging Sink items +- ~~`SEARCH`~~ ## Possible Links 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 index 24628aec..f5ca12a8 100644 --- 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 @@ -7,13 +7,13 @@ A Google Cloud Monitoring Alert Policy is a configuration object that defines th **Terrafrom Mappings:** -* `google_monitoring_alert_policy.id` +- `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). +- `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 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 index be3446f8..98225cf8 100644 --- 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 @@ -8,10 +8,10 @@ For full details, see the official documentation: https://cloud.google.com/monit **Terrafrom Mappings:** -* `google_monitoring_dashboard.id` +- `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. +- `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 index 2f1545f9..fab21d69 100644 --- 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 @@ -7,13 +7,13 @@ A **Google Cloud Monitoring Notification Channel** specifies where and how Cloud **Terrafrom Mappings:** -* `google_monitoring_notification_channel.name` +- `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). +- `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). ## Possible Links 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 index 991c60cc..9639146b 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-orgpolicy-policy.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-orgpolicy-policy.md @@ -8,13 +8,13 @@ For full details see Google’s official documentation: https://cloud.google.com **Terrafrom Mappings:** -* `google_org_policy_policy.name` +- `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). +- `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). ## Possible Links 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 index 7569096c..4b358d98 100644 --- 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 @@ -7,16 +7,16 @@ A Google Cloud Pub/Sub subscription represents a stream of messages delivered fr **Terrafrom Mappings:** -* `google_pubsub_subscription.name` -* `google_pubsub_subscription_iam_binding.subscription` -* `google_pubsub_subscription_iam_member.subscription` -* `google_pubsub_subscription_iam_policy.subscription` +- `google_pubsub_subscription.name` +- `google_pubsub_subscription_iam_binding.subscription` +- `google_pubsub_subscription_iam_member.subscription` +- `google_pubsub_subscription_iam_policy.subscription` ## Supported Methods -* `GET`: Get a gcp-pub-sub-subscription by its "name" -* `LIST`: List all gcp-pub-sub-subscription -* ~~`SEARCH`~~ +- `GET`: Get a gcp-pub-sub-subscription by its "name" +- `LIST`: List all gcp-pub-sub-subscription +- ~~`SEARCH`~~ ## Possible Links 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 index 7d63dced..9434dd26 100644 --- 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 @@ -8,16 +8,16 @@ For comprehensive information, see the official documentation: https://cloud.goo **Terrafrom Mappings:** -* `google_pubsub_topic.name` -* `google_pubsub_topic_iam_binding.topic` -* `google_pubsub_topic_iam_member.topic` -* `google_pubsub_topic_iam_policy.topic` +- `google_pubsub_topic.name` +- `google_pubsub_topic_iam_binding.topic` +- `google_pubsub_topic_iam_member.topic` +- `google_pubsub_topic_iam_policy.topic` ## Supported Methods -* `GET`: Get a gcp-pub-sub-topic by its "name" -* `LIST`: List all gcp-pub-sub-topic -* ~~`SEARCH`~~ +- `GET`: Get a gcp-pub-sub-topic by its "name" +- `LIST`: List all gcp-pub-sub-topic +- ~~`SEARCH`~~ ## Possible Links 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 index d9da7a85..ef6259bf 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-redis-instance.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-redis-instance.md @@ -7,13 +7,13 @@ A GCP Redis Instance is a fully managed, in-memory data store provided by Cloud **Terrafrom Mappings:** -* `google_redis_instance.id` +- `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. +- `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 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 index 8d483720..1e7b424b 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-revision.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-revision.md @@ -8,9 +8,9 @@ Official documentation: https://cloud.google.com/run/docs/reference/rest/v1/name ## Supported Methods -* `GET`: Get a gcp-run-revision by its "locations|services|revisions" -* ~~`LIST`~~ -* `SEARCH`: Search for gcp-run-revision by its "locations|services" +- `GET`: Get a gcp-run-revision by its "locations|services|revisions" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-run-revision by its "locations|services" ## Possible Links 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 index c0db35f3..6620ad19 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-service.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-run-service.md @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/run/docs **Terrafrom Mappings:** -* `google_cloud_run_v2_service.id` +- `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" +- `GET`: Get a gcp-run-service by its "locations|services" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-run-service by its "locations" ## Possible Links 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 index cb42e674..6c8018f7 100644 --- 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 @@ -8,13 +8,13 @@ For full details see the official documentation: https://cloud.google.com/secret **Terrafrom Mappings:** -* `google_secret_manager_secret.secret_id` +- `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`~~ +- `GET`: Get a gcp-secret-manager-secret by its "name" +- `LIST`: List all gcp-secret-manager-secret +- ~~`SEARCH`~~ ## Possible Links 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 index 0337b436..25501a0c 100644 --- 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 @@ -9,9 +9,9 @@ Official documentation: https://cloud.google.com/security-command-center/docs/re ## 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". +- `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". ## Possible Links 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 index 280899ba..76976f02 100644 --- 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 @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/service-directory/docs/referenc **Terrafrom Mappings:** -* `google_service_directory_endpoint.id` +- `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. +- `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 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 index a9d5d664..64ceb440 100644 --- 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 @@ -8,9 +8,9 @@ Official documentation: https://cloud.google.com/service-usage/docs/reference/re ## Supported Methods -* `GET`: Get a gcp-service-usage-service by its "name" -* `LIST`: List all gcp-service-usage-service -* ~~`SEARCH`~~ +- `GET`: Get a gcp-service-usage-service by its "name" +- `LIST`: List all gcp-service-usage-service +- ~~`SEARCH`~~ ## Possible Links 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 index 53a90810..76d315a0 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-database.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-database.md @@ -7,13 +7,13 @@ A GCP Spanner Database is a logically isolated collection of relational data tha **Terrafrom Mappings:** -* `google_spanner_database.name` +- `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" +- `GET`: Get a gcp-spanner-database by its "instances|databases" +- ~~`LIST`~~ +- `SEARCH`: Search for gcp-spanner-database by its "instances" ## Possible Links 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 index cf6337d0..67c03c71 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-instance.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-spanner-instance.md @@ -8,13 +8,13 @@ For full details see the official documentation: https://cloud.google.com/spanne **Terrafrom Mappings:** -* `google_spanner_instance.name` +- `google_spanner_instance.name` ## Supported Methods -* `GET`: Get a gcp-spanner-instance by its "name" -* `LIST`: List all gcp-spanner-instance -* ~~`SEARCH`~~ +- `GET`: Get a gcp-spanner-instance by its "name" +- `LIST`: List all gcp-spanner-instance +- ~~`SEARCH`~~ ## Possible Links 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 index 88892ae6..b5ce83cf 100644 --- 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 @@ -8,9 +8,9 @@ Official documentation: https://cloud.google.com/sql/docs/mysql/admin-api/rest/v ## 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" +- `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 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 index 40dbbf64..ab2ee094 100644 --- 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 @@ -8,9 +8,9 @@ See the official documentation for details: https://cloud.google.com/sql/docs/my ## Supported Methods -* `GET`: Get a gcp-sql-admin-backup by its "name" -* `LIST`: List all gcp-sql-admin-backup -* ~~`SEARCH`~~ +- `GET`: Get a gcp-sql-admin-backup by its "name" +- `LIST`: List all gcp-sql-admin-backup +- ~~`SEARCH`~~ ## Possible Links 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 index 82d68edb..c4167965 100644 --- 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 @@ -7,13 +7,13 @@ A Google Cloud SQL Admin Instance represents a fully-managed relational database **Terrafrom Mappings:** -* `google_sql_database_instance.name` +- `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`~~ +- `GET`: Get a gcp-sql-admin-instance by its "name" +- `LIST`: List all gcp-sql-admin-instance +- ~~`SEARCH`~~ ## Possible Links diff --git a/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket-iam-policy.md b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket-iam-policy.md index 0dd44a59..c919de54 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket-iam-policy.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket-iam-policy.md @@ -7,15 +7,15 @@ A **Storage Bucket IAM policy** defines who (principals) can perform which actio **Terrafrom Mappings:** -* `google_storage_bucket_iam_binding.bucket` -* `google_storage_bucket_iam_member.bucket` -* `google_storage_bucket_iam_policy.bucket` +- `google_storage_bucket_iam_binding.bucket` +- `google_storage_bucket_iam_member.bucket` +- `google_storage_bucket_iam_policy.bucket` ## Supported Methods -* `GET`: Get GCP Storage Bucket Iam Policy by "gcp-storage-bucket-iam-policy-bucket" -* ~~`LIST`~~ -* `SEARCH`: Search for GCP Storage Bucket Iam Policy by "gcp-storage-bucket-iam-policy-bucket" +- `GET`: Get GCP Storage Bucket Iam Policy by "gcp-storage-bucket-iam-policy-bucket" +- ~~`LIST`~~ +- `SEARCH`: Search for GCP Storage Bucket Iam Policy by "gcp-storage-bucket-iam-policy-bucket" ## Possible Links 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 index 2e0ecf48..2efbb013 100644 --- a/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket.md +++ b/docs.overmind.tech/docs/sources/gcp/Types/gcp-storage-bucket.md @@ -8,16 +8,16 @@ For full details see the official documentation: https://cloud.google.com/storag **Terrafrom Mappings:** -* `google_storage_bucket.name` -* `google_storage_bucket_iam_binding.bucket` -* `google_storage_bucket_iam_member.bucket` -* `google_storage_bucket_iam_policy.bucket` +- `google_storage_bucket.name` +- `google_storage_bucket_iam_binding.bucket` +- `google_storage_bucket_iam_member.bucket` +- `google_storage_bucket_iam_policy.bucket` ## Supported Methods -* `GET`: Get a gcp-storage-bucket by its "name" -* `LIST`: List all gcp-storage-bucket -* ~~`SEARCH`~~ +- `GET`: Get a gcp-storage-bucket by its "name" +- `LIST`: List all gcp-storage-bucket +- ~~`SEARCH`~~ ## Possible Links 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 index f0046429..6a2b63ff 100644 --- 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 @@ -8,13 +8,13 @@ Official documentation: https://cloud.google.com/storage-transfer/docs/create-tr **Terrafrom Mappings:** -* `google_storage_transfer_job.name` +- `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`~~ +- `GET`: Get a gcp-storage-transfer-transfer-job by its "name" +- `LIST`: List all gcp-storage-transfer-transfer-job +- ~~`SEARCH`~~ ## Possible Links diff --git a/docs.overmind.tech/docs/sources/gcp/configuration.md b/docs.overmind.tech/docs/sources/gcp/configuration.md index 57643214..4d56efcc 100644 --- a/docs.overmind.tech/docs/sources/gcp/configuration.md +++ b/docs.overmind.tech/docs/sources/gcp/configuration.md @@ -393,7 +393,7 @@ Here are all the predefined GCP roles that Overmind requires, plus the custom ro | Role | Purpose | | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `roles/browser` | **Required:** List projects and navigate resource hierarchy [GCP Docs](https://cloud.google.com/iam/docs/understanding-roles#browser) | +| `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) | 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 index 533f721d..14ebbab7 100644 --- 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 @@ -17,4 +17,4 @@ "search": true, "searchDescription": "Search Batch Prediction Jobs within a location. Use the location name e.g., 'us-central1'" } -} \ No newline at end of file +} 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 index 178705cb..6301d3ec 100644 --- 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 @@ -16,4 +16,4 @@ "list": true, "listDescription": "List all gcp-ai-platform-custom-job" } -} \ No newline at end of file +} 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 index 9bb17e71..ff51ee9d 100644 --- 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 @@ -16,4 +16,4 @@ "list": true, "listDescription": "List all gcp-ai-platform-endpoint" } -} \ No newline at end of file +} 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 index 5f618ed7..7de24b4a 100644 --- 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 @@ -16,4 +16,4 @@ "search": true, "searchDescription": "Search Model Deployment Monitoring Jobs within a location. Use the location name e.g., 'us-central1'" } -} \ No newline at end of file +} 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 index 24c3254c..2b6f499f 100644 --- 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 @@ -15,4 +15,4 @@ "list": true, "listDescription": "List all gcp-ai-platform-model" } -} \ No newline at end of file +} 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 index ebc9dbf7..48d48bc7 100644 --- 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 @@ -14,4 +14,4 @@ "list": true, "listDescription": "List all gcp-ai-platform-pipeline-job" } -} \ No newline at end of file +} 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 index f6430a93..1c51945e 100644 --- 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 @@ -14,4 +14,4 @@ "terraformQueryMap": "google_artifact_registry_docker_image.name" } ] -} \ No newline at end of file +} 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 index 00e50fce..db2af391 100644 --- 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 @@ -20,4 +20,4 @@ "terraformQueryMap": "google_bigquery_data_transfer_config.id" } ] -} \ No newline at end of file +} 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 index 348cb790..d9a0685a 100644 --- 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 @@ -29,4 +29,4 @@ "terraformQueryMap": "google_bigquery_dataset_iam_policy.dataset_id" } ] -} \ No newline at end of file +} 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 index 25731ea9..76adabc7 100644 --- 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 @@ -1,10 +1,7 @@ { "type": "gcp-big-query-routine", "category": 6, - "potentialLinks": [ - "gcp-big-query-dataset", - "gcp-storage-bucket" - ], + "potentialLinks": ["gcp-big-query-dataset", "gcp-storage-bucket"], "descriptiveName": "GCP Big Query Routine", "supportedQueryMethods": { "get": true, @@ -18,4 +15,4 @@ "terraformQueryMap": "google_bigquery_routine.id" } ] -} \ No newline at end of file +} 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 index cbb926c0..973275de 100644 --- 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 @@ -32,4 +32,4 @@ "terraformQueryMap": "google_bigquery_table_iam_policy.dataset_id" } ] -} \ No newline at end of file +} 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 index c6c3e240..4358f119 100644 --- 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 @@ -18,4 +18,4 @@ "terraformQueryMap": "google_bigtable_app_profile.id" } ] -} \ No newline at end of file +} 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 index bc06d5ee..30d81630 100644 --- 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 @@ -13,4 +13,4 @@ "search": true, "searchDescription": "Search for gcp-big-table-admin-backup by its \"instances|clusters\"" } -} \ No newline at end of file +} 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 index 37d238de..bb3be7f6 100644 --- 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 @@ -12,4 +12,4 @@ "search": true, "searchDescription": "Search for gcp-big-table-admin-cluster by its \"instances\"" } -} \ No newline at end of file +} 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 index fff15c10..aca434bd 100644 --- 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 @@ -1,9 +1,7 @@ { "type": "gcp-big-table-admin-instance", "category": 7, - "potentialLinks": [ - "gcp-big-table-admin-cluster" - ], + "potentialLinks": ["gcp-big-table-admin-cluster"], "descriptiveName": "GCP Big Table Admin Instance", "supportedQueryMethods": { "get": true, @@ -25,4 +23,4 @@ "terraformQueryMap": "google_bigtable_instance_iam_policy.instance" } ] -} \ No newline at end of file +} 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 index c24314d6..7816c24b 100644 --- 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 @@ -31,4 +31,4 @@ "terraformQueryMap": "google_bigtable_table_iam_policy.instance_name" } ] -} \ No newline at end of file +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-certificate-manager-certificate.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-certificate-manager-certificate.json index 0a31cbc1..ebaf5d4c 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-certificate-manager-certificate.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-certificate-manager-certificate.json @@ -14,4 +14,4 @@ "terraformQueryMap": "google_certificate_manager_certificate.id" } ] -} \ No newline at end of file +} 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 index 8d13c29c..d34a018f 100644 --- 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 @@ -1,12 +1,10 @@ { "type": "gcp-cloud-billing-billing-info", "category": 7, - "potentialLinks": [ - "gcp-cloud-resource-manager-project" - ], + "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\"" } -} \ No newline at end of file +} 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 index 6e88a687..e2c3657a 100644 --- 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 @@ -16,4 +16,4 @@ "list": true, "listDescription": "List all gcp-cloud-build-build" } -} \ No newline at end of file +} 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 index 8c31985f..aa542ad7 100644 --- 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 @@ -15,4 +15,4 @@ "search": true, "searchDescription": "Search for gcp-cloud-functions-function by its \"locations\"" } -} \ No newline at end of file +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key-version.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key-version.json index 5f931fe5..a7a95898 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key-version.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-cloud-kms-crypto-key-version.json @@ -1,9 +1,7 @@ { "type": "gcp-cloud-kms-crypto-key-version", "category": 4, - "potentialLinks": [ - "gcp-cloud-kms-crypto-key" - ], + "potentialLinks": ["gcp-cloud-kms-crypto-key"], "descriptiveName": "GCP Cloud Kms Crypto Key Version", "supportedQueryMethods": { "get": true, @@ -17,4 +15,4 @@ "terraformQueryMap": "google_kms_crypto_key_version.id" } ] -} \ No newline at end of file +} 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 index 1995c3de..f8eb5e0c 100644 --- 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 @@ -18,4 +18,4 @@ "terraformQueryMap": "google_kms_crypto_key.id" } ] -} \ No newline at end of file +} 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 index c2376a47..76173476 100644 --- 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 @@ -1,9 +1,7 @@ { "type": "gcp-cloud-kms-key-ring", "category": 4, - "potentialLinks": [ - "gcp-cloud-kms-crypto-key" - ], + "potentialLinks": ["gcp-cloud-kms-crypto-key"], "descriptiveName": "GCP Cloud Kms Key Ring", "supportedQueryMethods": { "get": true, @@ -19,4 +17,4 @@ "terraformQueryMap": "google_kms_key_ring.id" } ] -} \ No newline at end of file +} 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 index 4e2ae5cf..61a6eb0b 100644 --- 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 @@ -6,4 +6,4 @@ "get": true, "getDescription": "Get a gcp-cloud-resource-manager-project by its \"name\"" } -} \ No newline at end of file +} 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 index 1edebcde..48c3491c 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_tags_tag_value.name" } ] -} \ No newline at end of file +} 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 index b15037cb..0d1ecaa1 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-address.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-address.json @@ -23,4 +23,4 @@ "terraformQueryMap": "google_compute_address.name" } ] -} \ No newline at end of file +} 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 index 79401514..41effcaf 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-autoscaler.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-autoscaler.json @@ -1,9 +1,7 @@ { "type": "gcp-compute-autoscaler", "category": 7, - "potentialLinks": [ - "gcp-compute-instance-group-manager" - ], + "potentialLinks": ["gcp-compute-instance-group-manager"], "descriptiveName": "GCP Compute Autoscaler", "supportedQueryMethods": { "get": true, @@ -16,4 +14,4 @@ "terraformQueryMap": "google_compute_autoscaler.name" } ] -} \ No newline at end of file +} 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 index 69e07b51..7d40a584 100644 --- 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 @@ -24,4 +24,4 @@ "terraformQueryMap": "google_compute_region_backend_service.name" } ] -} \ No newline at end of file +} 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 index 03c34339..9f916ca9 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-disk.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-disk.json @@ -22,4 +22,4 @@ "terraformQueryMap": "google_compute_disk.name" } ] -} \ No newline at end of file +} 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 index 644702b3..2ee555ba 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_compute_external_vpn_gateway.name" } ] -} \ No newline at end of file +} 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 index 9f7b4208..6cdeae3f 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-firewall.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-firewall.json @@ -20,4 +20,4 @@ "terraformQueryMap": "google_compute_firewall.name" } ] -} \ No newline at end of file +} 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 index 5342f4f2..c4ef38d6 100644 --- 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 @@ -23,4 +23,4 @@ "terraformQueryMap": "google_compute_forwarding_rule.name" } ] -} \ No newline at end of file +} 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 index 0072832c..84a97610 100644 --- 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 @@ -18,4 +18,4 @@ "terraformQueryMap": "google_compute_global_address.name" } ] -} \ No newline at end of file +} 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 index e1125106..2700eba7 100644 --- 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 @@ -19,4 +19,4 @@ "terraformQueryMap": "google_compute_global_forwarding_rule.name" } ] -} \ No newline at end of file +} 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 index cdeafd85..6f36f867 100644 --- 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 @@ -16,4 +16,4 @@ "terraformQueryMap": "google_compute_region_health_check.name" } ] -} \ No newline at end of file +} 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 index 1965aa77..f5d21e72 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_compute_http_health_check.name" } ] -} \ No newline at end of file +} 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 index fdd276a7..427ff58a 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-image.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-image.json @@ -24,4 +24,4 @@ "terraformQueryMap": "google_compute_image.name" } ] -} \ No newline at end of file +} 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 index 3c6f5b4d..566c1414 100644 --- 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 @@ -20,4 +20,4 @@ "terraformQueryMap": "google_compute_instance_group_manager.name" } ] -} \ No newline at end of file +} 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 index 72795236..0368fa88 100644 --- 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 @@ -1,10 +1,7 @@ { "type": "gcp-compute-instance-group", "category": 1, - "potentialLinks": [ - "gcp-compute-network", - "gcp-compute-subnetwork" - ], + "potentialLinks": ["gcp-compute-network", "gcp-compute-subnetwork"], "descriptiveName": "GCP Compute Instance Group", "supportedQueryMethods": { "get": true, @@ -17,4 +14,4 @@ "terraformQueryMap": "google_compute_instance_group.name" } ] -} \ No newline at end of file +} 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 index 864330b3..5b335a19 100644 --- 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 @@ -30,4 +30,4 @@ "terraformQueryMap": "google_compute_instance_template.name" } ] -} \ No newline at end of file +} 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 index 0f1b04ff..b734dbe4 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-instance.json @@ -29,4 +29,4 @@ "terraformQueryMap": "google_compute_instance.name" } ] -} \ No newline at end of file +} 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 index f0007c97..a37e8bd6 100644 --- 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 @@ -1,9 +1,7 @@ { "type": "gcp-compute-instant-snapshot", "category": 2, - "potentialLinks": [ - "gcp-compute-disk" - ], + "potentialLinks": ["gcp-compute-disk"], "descriptiveName": "GCP Compute Instant Snapshot", "supportedQueryMethods": { "get": true, @@ -16,4 +14,4 @@ "terraformQueryMap": "google_compute_instant_snapshot.name" } ] -} \ No newline at end of file +} 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 index 6ae291c0..a34228ff 100644 --- 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 @@ -23,4 +23,4 @@ "terraformQueryMap": "google_compute_machine_image.name" } ] -} \ No newline at end of file +} 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 index cbe1024e..cf4fa2ae 100644 --- 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 @@ -19,4 +19,4 @@ "terraformQueryMap": "google_compute_network_endpoint_group.name" } ] -} \ No newline at end of file +} 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 index 76e7a4d7..f4e5d730 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-network.json @@ -1,10 +1,7 @@ { "type": "gcp-compute-network", "category": 3, - "potentialLinks": [ - "gcp-compute-network", - "gcp-compute-subnetwork" - ], + "potentialLinks": ["gcp-compute-network", "gcp-compute-subnetwork"], "descriptiveName": "GCP Compute Network", "supportedQueryMethods": { "get": true, @@ -17,4 +14,4 @@ "terraformQueryMap": "google_compute_network.name" } ] -} \ No newline at end of file +} 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 index 9787e141..aa2b9ac3 100644 --- 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 @@ -1,9 +1,7 @@ { "type": "gcp-compute-node-group", "category": 1, - "potentialLinks": [ - "gcp-compute-node-template" - ], + "potentialLinks": ["gcp-compute-node-template"], "descriptiveName": "GCP Compute Node Group", "supportedQueryMethods": { "get": true, @@ -22,4 +20,4 @@ "terraformQueryMap": "google_compute_node_template.name" } ] -} \ No newline at end of file +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-template.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-template.json index b488d338..cb5ddf88 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-template.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-node-template.json @@ -1,9 +1,7 @@ { "type": "gcp-compute-node-template", "category": 7, - "potentialLinks": [ - "gcp-compute-node-group" - ], + "potentialLinks": ["gcp-compute-node-group"], "descriptiveName": "GCP Compute Node Template", "supportedQueryMethods": { "get": true, @@ -16,4 +14,4 @@ "terraformQueryMap": "google_compute_node_template.name" } ] -} \ No newline at end of file +} 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 index 55885cc9..b0ecba12 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-project.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-project.json @@ -1,10 +1,7 @@ { "type": "gcp-compute-project", "category": 7, - "potentialLinks": [ - "gcp-iam-service-account", - "gcp-storage-bucket" - ], + "potentialLinks": ["gcp-iam-service-account", "gcp-storage-bucket"], "descriptiveName": "GCP Compute Project", "supportedQueryMethods": { "get": true, @@ -36,4 +33,4 @@ "terraformQueryMap": "google_project_iam_audit_config.project" } ] -} \ No newline at end of file +} 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 index e213916a..7348176c 100644 --- 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 @@ -20,4 +20,4 @@ "terraformQueryMap": "google_compute_public_delegated_prefix.id" } ] -} \ No newline at end of file +} 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 index c9862743..56244bf7 100644 --- 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 @@ -1,8 +1,6 @@ { "type": "gcp-compute-region-commitment", - "potentialLinks": [ - "gcp-compute-reservation" - ], + "potentialLinks": ["gcp-compute-reservation"], "descriptiveName": "GCP Compute Region Commitment", "supportedQueryMethods": { "get": true, @@ -15,4 +13,4 @@ "terraformQueryMap": "google_compute_region_commitment.name" } ] -} \ No newline at end of file +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-regional-instance-group-manager.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-regional-instance-group-manager.json index 802da3df..4dd43820 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-regional-instance-group-manager.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-regional-instance-group-manager.json @@ -20,4 +20,4 @@ "terraformQueryMap": "google_compute_region_instance_group_manager.name" } ] -} \ No newline at end of file +} 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 index 94e395e6..b8946169 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-reservation.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-reservation.json @@ -1,9 +1,7 @@ { "type": "gcp-compute-reservation", "category": 1, - "potentialLinks": [ - "gcp-compute-region-commitment" - ], + "potentialLinks": ["gcp-compute-region-commitment"], "descriptiveName": "GCP Compute Reservation", "supportedQueryMethods": { "get": true, @@ -16,4 +14,4 @@ "terraformQueryMap": "google_compute_reservation.name" } ] -} \ No newline at end of file +} 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 index 4172966d..6f15881d 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-route.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-route.json @@ -21,4 +21,4 @@ "terraformQueryMap": "google_compute_route.name" } ] -} \ No newline at end of file +} 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 index 962ebed7..525bec37 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-router.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-router.json @@ -21,4 +21,4 @@ "terraformQueryMap": "google_compute_router.id" } ] -} \ No newline at end of file +} 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 index 2c451384..b2a5b7e5 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_compute_security_policy.name" } ] -} \ No newline at end of file +} 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 index 8bd46da1..aafae65c 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-snapshot.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-snapshot.json @@ -18,4 +18,4 @@ "terraformQueryMap": "google_compute_snapshot.name" } ] -} \ No newline at end of file +} 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 index 289ad656..d2b248fc 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_compute_ssl_certificate.name" } ] -} \ No newline at end of file +} 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 index 842004a7..37962175 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_compute_ssl_policy.name" } ] -} \ No newline at end of file +} 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 index 50a6ae34..2e2e09e5 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-subnetwork.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-compute-subnetwork.json @@ -17,4 +17,4 @@ "terraformQueryMap": "google_compute_subnetwork.name" } ] -} \ No newline at end of file +} 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 index 5243798c..866c2fe2 100644 --- 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 @@ -1,9 +1,7 @@ { "type": "gcp-compute-target-http-proxy", "category": 3, - "potentialLinks": [ - "gcp-compute-url-map" - ], + "potentialLinks": ["gcp-compute-url-map"], "descriptiveName": "GCP Compute Target Http Proxy", "supportedQueryMethods": { "get": true, @@ -16,4 +14,4 @@ "terraformQueryMap": "google_compute_target_http_proxy.name" } ] -} \ No newline at end of file +} 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 index 091c7fda..329ff140 100644 --- 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 @@ -18,4 +18,4 @@ "terraformQueryMap": "google_compute_target_https_proxy.name" } ] -} \ No newline at end of file +} 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 index 71790f0f..d4af55ad 100644 --- 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 @@ -21,4 +21,4 @@ "terraformQueryMap": "google_compute_target_pool.id" } ] -} \ No newline at end of file +} 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 index d070939b..e5ae1cc4 100644 --- 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 @@ -1,9 +1,7 @@ { "type": "gcp-compute-url-map", "category": 3, - "potentialLinks": [ - "gcp-compute-backend-service" - ], + "potentialLinks": ["gcp-compute-backend-service"], "descriptiveName": "GCP Compute Url Map", "supportedQueryMethods": { "get": true, @@ -16,4 +14,4 @@ "terraformQueryMap": "google_compute_url_map.name" } ] -} \ No newline at end of file +} 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 index 6cb2f48d..29de0dae 100644 --- 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 @@ -1,9 +1,7 @@ { "type": "gcp-compute-vpn-gateway", "category": 3, - "potentialLinks": [ - "gcp-compute-network" - ], + "potentialLinks": ["gcp-compute-network"], "descriptiveName": "GCP Compute Vpn Gateway", "supportedQueryMethods": { "get": true, @@ -16,4 +14,4 @@ "terraformQueryMap": "google_compute_ha_vpn_gateway.name" } ] -} \ No newline at end of file +} 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 index ddcc1aec..10d8198b 100644 --- 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 @@ -18,4 +18,4 @@ "terraformQueryMap": "google_compute_vpn_tunnel.name" } ] -} \ No newline at end of file +} 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 index 31891141..a981a63f 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-container-cluster.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-container-cluster.json @@ -25,4 +25,4 @@ "terraformQueryMap": "google_container_cluster.id" } ] -} \ No newline at end of file +} 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 index a50aa39e..10bebbea 100644 --- 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 @@ -23,4 +23,4 @@ "terraformQueryMap": "google_container_node_pool.id" } ] -} \ No newline at end of file +} 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 index ffba7ab9..c83c8dc1 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataform-repository.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataform-repository.json @@ -20,4 +20,4 @@ "terraformQueryMap": "google_dataform_repository.id" } ] -} \ No newline at end of file +} 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 index 8bb7d290..9d600015 100644 --- 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 @@ -14,4 +14,4 @@ "terraformQueryMap": "google_dataplex_aspect_type.id" } ] -} \ No newline at end of file +} 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 index ddd66d24..0f0e2d11 100644 --- 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 @@ -1,10 +1,7 @@ { "type": "gcp-dataplex-data-scan", "category": 5, - "potentialLinks": [ - "gcp-big-query-table", - "gcp-storage-bucket" - ], + "potentialLinks": ["gcp-big-query-table", "gcp-storage-bucket"], "descriptiveName": "GCP Dataplex Data Scan", "supportedQueryMethods": { "get": true, @@ -18,4 +15,4 @@ "terraformQueryMap": "google_dataplex_datascan.id" } ] -} \ No newline at end of file +} 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 index 1ab6ee27..22f88da0 100644 --- 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 @@ -14,4 +14,4 @@ "terraformQueryMap": "google_dataplex_entry_group.id" } ] -} \ No newline at end of file +} 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 index efa1b07d..05e71e51 100644 --- 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 @@ -13,4 +13,4 @@ "terraformQueryMap": "google_dataproc_autoscaling_policy.name" } ] -} \ No newline at end of file +} 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 index 0482caa7..ffe69314 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-cluster.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-dataproc-cluster.json @@ -27,4 +27,4 @@ "terraformQueryMap": "google_dataproc_cluster.name" } ] -} \ No newline at end of file +} 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 index 98f8ab9d..54e61a26 100644 --- 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 @@ -1,10 +1,7 @@ { "type": "gcp-dns-managed-zone", "category": 3, - "potentialLinks": [ - "gcp-compute-network", - "gcp-container-cluster" - ], + "potentialLinks": ["gcp-compute-network", "gcp-container-cluster"], "descriptiveName": "GCP Dns Managed Zone", "supportedQueryMethods": { "get": true, @@ -17,4 +14,4 @@ "terraformQueryMap": "google_dns_managed_zone.name" } ] -} \ No newline at end of file +} 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 index 76dd3031..da7a9ff2 100644 --- 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 @@ -15,4 +15,4 @@ "terraformQueryMap": "google_essential_contacts_contact.id" } ] -} \ No newline at end of file +} 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 index 5dfae89f..1a538cc3 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-file-instance.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-file-instance.json @@ -1,10 +1,7 @@ { "type": "gcp-file-instance", "category": 2, - "potentialLinks": [ - "gcp-cloud-kms-crypto-key", - "gcp-compute-network" - ], + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-compute-network"], "descriptiveName": "GCP File Instance", "supportedQueryMethods": { "get": true, @@ -18,4 +15,4 @@ "terraformQueryMap": "google_filestore_instance.id" } ] -} \ No newline at end of file +} 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 index bf0ce0e2..1f4396c5 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-role.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-iam-role.json @@ -8,4 +8,4 @@ "list": true, "listDescription": "List all gcp-iam-role" } -} \ No newline at end of file +} 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 index 48fcb53c..c75970c7 100644 --- 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 @@ -1,9 +1,7 @@ { "type": "gcp-iam-service-account-key", "category": 4, - "potentialLinks": [ - "gcp-iam-service-account" - ], + "potentialLinks": ["gcp-iam-service-account"], "descriptiveName": "GCP Iam Service Account Key", "supportedQueryMethods": { "get": true, @@ -17,4 +15,4 @@ "terraformQueryMap": "google_service_account_key.id" } ] -} \ No newline at end of file +} 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 index 2f6f6f50..f4e8078e 100644 --- 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 @@ -20,4 +20,4 @@ "terraformQueryMap": "google_service_account.unique_id" } ] -} \ No newline at end of file +} 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 index 75c77fc3..0610c01f 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-bucket.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-bucket.json @@ -13,4 +13,4 @@ "search": true, "searchDescription": "Search for gcp-logging-bucket by its \"locations\"" } -} \ No newline at end of file +} 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 index 8895fca1..319a71a0 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-link.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-link.json @@ -1,10 +1,7 @@ { "type": "gcp-logging-link", "category": 5, - "potentialLinks": [ - "gcp-big-query-dataset", - "gcp-logging-bucket" - ], + "potentialLinks": ["gcp-big-query-dataset", "gcp-logging-bucket"], "descriptiveName": "GCP Logging Link", "supportedQueryMethods": { "get": true, @@ -12,4 +9,4 @@ "search": true, "searchDescription": "Search for gcp-logging-link by its \"locations|buckets\"" } -} \ No newline at end of file +} 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 index 75db0a53..f68dd359 100644 --- 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 @@ -8,4 +8,4 @@ "search": true, "searchDescription": "Search for gcp-logging-saved-query by its \"locations\"" } -} \ No newline at end of file +} 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 index f80a43f5..281a1984 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-sink.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-logging-sink.json @@ -15,4 +15,4 @@ "list": true, "listDescription": "List all GCP Logging Sink items" } -} \ No newline at end of file +} 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 index e3319196..89297235 100644 --- 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 @@ -1,9 +1,7 @@ { "type": "gcp-monitoring-alert-policy", "category": 5, - "potentialLinks": [ - "gcp-monitoring-notification-channel" - ], + "potentialLinks": ["gcp-monitoring-notification-channel"], "descriptiveName": "GCP Monitoring Alert Policy", "supportedQueryMethods": { "get": true, @@ -19,4 +17,4 @@ "terraformQueryMap": "google_monitoring_alert_policy.id" } ] -} \ No newline at end of file +} 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 index ed6ef2b3..5dcb1ef2 100644 --- 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 @@ -16,4 +16,4 @@ "terraformQueryMap": "google_monitoring_dashboard.id" } ] -} \ No newline at end of file +} 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 index 0153d49b..04d9ca96 100644 --- 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 @@ -1,9 +1,7 @@ { "type": "gcp-monitoring-notification-channel", "category": 5, - "potentialLinks": [ - "gcp-pub-sub-topic" - ], + "potentialLinks": ["gcp-pub-sub-topic"], "descriptiveName": "GCP Monitoring Notification Channel", "supportedQueryMethods": { "get": true, @@ -18,4 +16,4 @@ "terraformQueryMap": "google_monitoring_notification_channel.name" } ] -} \ No newline at end of file +} 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 index a61c8d26..e45f413f 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-orgpolicy-policy.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-orgpolicy-policy.json @@ -1,9 +1,7 @@ { "type": "gcp-orgpolicy-policy", "category": 7, - "potentialLinks": [ - "gcp-cloud-resource-manager-project" - ], + "potentialLinks": ["gcp-cloud-resource-manager-project"], "descriptiveName": "GCP Orgpolicy Policy", "supportedQueryMethods": { "get": true, @@ -19,4 +17,4 @@ "terraformQueryMap": "google_org_policy_policy.name" } ] -} \ No newline at end of file +} 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 index 39649d1e..8ffa0e1a 100644 --- 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 @@ -29,4 +29,4 @@ "terraformQueryMap": "google_pubsub_subscription_iam_policy.subscription" } ] -} \ No newline at end of file +} 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 index 467457e8..4d18093e 100644 --- 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 @@ -27,4 +27,4 @@ "terraformQueryMap": "google_pubsub_topic_iam_policy.topic" } ] -} \ No newline at end of file +} 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 index ffcf3fdf..96b1fcd6 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-redis-instance.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-redis-instance.json @@ -19,4 +19,4 @@ "terraformQueryMap": "google_redis_instance.id" } ] -} \ No newline at end of file +} 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 index 6768f584..ee50a137 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-run-revision.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-run-revision.json @@ -19,4 +19,4 @@ "search": true, "searchDescription": "Search for gcp-run-revision by its \"locations|services\"" } -} \ No newline at end of file +} 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 index 9bdb3abb..74851f26 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-run-service.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-run-service.json @@ -25,4 +25,4 @@ "terraformQueryMap": "google_cloud_run_v2_service.id" } ] -} \ No newline at end of file +} 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 index e169de67..c85bf1d8 100644 --- 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 @@ -1,10 +1,7 @@ { "type": "gcp-secret-manager-secret", "category": 4, - "potentialLinks": [ - "gcp-cloud-kms-crypto-key", - "gcp-pub-sub-topic" - ], + "potentialLinks": ["gcp-cloud-kms-crypto-key", "gcp-pub-sub-topic"], "descriptiveName": "GCP Secret Manager Secret", "supportedQueryMethods": { "get": true, @@ -17,4 +14,4 @@ "terraformQueryMap": "google_secret_manager_secret.secret_id" } ] -} \ No newline at end of file +} 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 index 5496fff4..997fcdc2 100644 --- 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 @@ -1,9 +1,7 @@ { "type": "gcp-security-center-management-security-center-service", "category": 4, - "potentialLinks": [ - "gcp-cloud-resource-manager-project" - ], + "potentialLinks": ["gcp-cloud-resource-manager-project"], "descriptiveName": "GCP Security Center Management Security Center Service", "supportedQueryMethods": { "get": true, @@ -11,4 +9,4 @@ "search": true, "searchDescription": "Search Security Center services in a location. Use the format \"location\"." } -} \ No newline at end of file +} 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 index ec76f68e..83acd69d 100644 --- 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 @@ -1,9 +1,7 @@ { "type": "gcp-service-directory-endpoint", "category": 7, - "potentialLinks": [ - "gcp-compute-network" - ], + "potentialLinks": ["gcp-compute-network"], "descriptiveName": "GCP Service Directory Endpoint", "supportedQueryMethods": { "get": true, @@ -17,4 +15,4 @@ "terraformQueryMap": "google_service_directory_endpoint.id" } ] -} \ No newline at end of file +} 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 index 53fa6f74..215f3451 100644 --- 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 @@ -1,10 +1,7 @@ { "type": "gcp-service-usage-service", "category": 7, - "potentialLinks": [ - "gcp-cloud-resource-manager-project", - "gcp-pub-sub-topic" - ], + "potentialLinks": ["gcp-cloud-resource-manager-project", "gcp-pub-sub-topic"], "descriptiveName": "GCP Service Usage Service", "supportedQueryMethods": { "get": true, @@ -12,4 +9,4 @@ "list": true, "listDescription": "List all gcp-service-usage-service" } -} \ No newline at end of file +} 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 index 27b3c0ad..8392d7e6 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-database.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-database.json @@ -19,4 +19,4 @@ "terraformQueryMap": "google_spanner_database.name" } ] -} \ No newline at end of file +} 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 index e44cc5d5..b3193800 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-instance.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-spanner-instance.json @@ -1,9 +1,7 @@ { "type": "gcp-spanner-instance", "category": 6, - "potentialLinks": [ - "gcp-spanner-database" - ], + "potentialLinks": ["gcp-spanner-database"], "descriptiveName": "GCP Spanner Instance", "supportedQueryMethods": { "get": true, @@ -16,4 +14,4 @@ "terraformQueryMap": "google_spanner_instance.name" } ] -} \ No newline at end of file +} 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 index f4a27d72..5ad4bb97 100644 --- 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 @@ -13,4 +13,4 @@ "search": true, "searchDescription": "Search for gcp-sql-admin-backup-run by its \"instances\"" } -} \ No newline at end of file +} 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 index ced944f2..989e4229 100644 --- 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 @@ -14,4 +14,4 @@ "list": true, "listDescription": "List all gcp-sql-admin-backup" } -} \ No newline at end of file +} 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 index b19914bf..c884f959 100644 --- 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 @@ -22,4 +22,4 @@ "terraformQueryMap": "google_sql_database_instance.name" } ] -} \ No newline at end of file +} diff --git a/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket-iam-policy.json b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket-iam-policy.json index 350a3274..82939923 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket-iam-policy.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket-iam-policy.json @@ -25,4 +25,4 @@ "terraformQueryMap": "google_storage_bucket_iam_policy.bucket" } ] -} \ No newline at end of file +} 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 index 57b9d8e3..e9db4d9e 100644 --- a/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket.json +++ b/docs.overmind.tech/docs/sources/gcp/data/gcp-storage-bucket.json @@ -28,4 +28,4 @@ "terraformQueryMap": "google_storage_bucket_iam_policy.bucket" } ] -} \ No newline at end of file +} 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 index 54242757..945d03b6 100644 --- 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 @@ -20,4 +20,4 @@ "terraformQueryMap": "google_storage_transfer_job.name" } ] -} \ No newline at end of file +} diff --git a/docs.overmind.tech/docs/sources/k8s/account_settings.png b/docs.overmind.tech/docs/sources/k8s/account_settings.png index 4e50b499f048c53aabd557cef52eeceaf5fcb30f..c7e184cb1e623fe6890a8390a5efd8cf94945e5d 100644 GIT binary patch literal 403511 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

<_@Raq2O1(n*u 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�PsY`KpTut7T(8@%4?2q zf=|ucct|vWR6TeBr5NbIhxxpR%g>R-4}Oe?IA#2{eI%>Tz*Zie4C^(A7*q0ye(xX% z*~nw8+m4r-)6*wO(2^|yfOW0(25vOePVHR*&4hQw&tuAjNP21hwST1c|6s_4m~2tQ z(VLV2_y-3a-LV5)nCE#ZgdIurEmO=|Dw5wsL`B^(Q@&oQJ%s7e1Bgpe#u4PB7nX=~ z@{tSW-?m%~Uh^!_CFy^MtuC3pYDliG^dCLCCy}Vv#d<@3%l^h~lGjJ~(UzBQ19w*) z)|MaW4r4z=vvBc9t$u;q=@DJ{oK9I8#OBO`tnJzza8g3pTM!cyAJOTvn?s2Orw3`W zDl;AJ(KpkLn_+A;UakiCp=Kr3ARI6PrxhCQ4Jp7-&U)v;SkwzFBL-NROxhfzVNIe@ zMZ1`I{>;N2byz<6&SCp|sjK`9q128;CkDwLz-$<$Em0j2>l*L%LN2C~9=4?mu@QtS zU2vP$@J;hTO{>oD&dZpFM}oF5&w|PE=$A@C^KeUL=f~(xA1KTKp=U8pf>wZiH^_?f ziLfz0I1TEQm?J+K1en&ABw66SG`*1Q4r#jDOld?O zUjJdCMIeyP41@c=AB-Ug^hehpI6J2x$N_hsYLoYXwc$R2(f$@BCB0dgjND+u0nZ;O zYaphxLn#ft7lX5$Awt1}Q@>42*G>VbA*(CQ+$<<2p0Y z5yss2z|J!J1DHKf|A4LyHp9{{SQ+S+cy}70YCm@QBKSsa6-|%Wy-2{_y%60(qF30m zSD20UcDfjdiHj5(l)^Bi`A#9D8Nzb7pya+TC$y0`q!UM{kB$43ic9QrUdh$+(Mvi{ zi`{w$T^m~vpr_NHACgC6JI%njVaXHbAjK7guATUl0%$wNSdoxV8R#e%=OFt)_@JYs zgQn*S;6B_G*}}N$F<4}(`~)_Tc=-7DI<%CdDUj<+U@%qELA8&?mt#T2uF$MWk1X~z z3q{W1ZdZ>6R1M6zwOb|iHcL>wuixugXR2gb-=L9rI#3ZzIgaQy`(@K$+yJqa{3(i0 zeZe<3KJk9O_F-mQuV9Xxzo(*f%p#0W7iN`8=->9s7?hq3p|7aAyF4kJ7~w)fH7#`8 z=z{8t;|oxTFjwRFR_CdxrJs3$&-gQOXe|JJ%ISM5I~jPGBY?a(g9JmJw@hg%P(t)j zT!Ad{N8JDEXElbFU5Kzq`=OJF=!1+rU|(@a@>jG$kR3j?h-R?%`8upP+CIQN9H*1= znZG^!7R>u38?FG_OI#WWQ@nnM#SafM9c%$s&>M3tmjfbzmoWt58L9+rbr|^mWb1L8)DBF&fTdPc+0`M4qhLc|#Tk{RNGI+t}B-7ADo8hzo zxihDr`P(lDe?J}~_4qKzwEx|AQt}K80)5ax(*j`yνFW?;WF>-Nw#vg`B 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

`WHqLU)vx!Yq~nuCsUfe& z^|s5Xx6io(m9iuFo__Hd2u|t^ews;B{U{i9*`Y%&yHK_)7}j1?GvS@SPaL+N^eqWF zG|Thab}Q%56bhfaC4cHddkEtD3;(gCOn0x zjdAxVttu~5wR)nj!{wU4&gP1&$TCjz;nZvo)AW5}P zL(AZU8@aiw&@W~&+=2@4b5U-ojB|Fui*-^yKdNL16Y8?Yl=XkZLLq%l_)OkM_~q%E zjni`XYH?d_!9$+(7{pw&UhaFdSK9CUJ6zZu&?b3AxM7r?^SCt|K zteLowL+oL%Z{L<4+-ERqixw*LY2m1Hmg_*#IbKNrMGexrA#bg}2ji_^%BW`59=M8l+Rk{X<4mE#_44BIzjK^AYEF)9=; ze=)yjJgmXN134!@VXnK;2aJ2h^RKPLB#%$BN|%_&%&y6mkVO<(?$o*uB{$WB;TN9z zN{BNZw+lZz381AElQf(2r;1-La5n>9tx_w`EV@Hcn>el!$$UZ!(8>i7pf};-H(ykT zP$AgoH@^YuVCopL@84lIn+VAh(E`Erc%7I}WSK6s`ZQ_Lj#*vDpzvLO zS|(*9AwY#BCv-P80!D6f&+zpY71+rasE^$=VCXqAXdKTLb<4O0db)K`J}{LCFexXP zdChoN-Hl*Y;?NRL4#D~gGg2VchswNj#_(hGP@%5E-4XTA{PxSej2-8=@l@}V{c#Sz z8215C(l~YvrfaUBA;WBxVyBPj=xwXwegt3EA@t+-b}J<*^!s29pv#j;uYZP3cAr)i zB|EAt8y1|;Eg!c7Y;Q>9vP%0DxI71{H<=cViI8IlPu}x24s~R(q#ADZc3d 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%0ArX!tDIgfi#M4Ea?( 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ⅆ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 literal 49931 zcmd3uhdW$N8}JcC3nE%b^xjsN=)DHf`|7<#@4ZIvH3(56MDME$qOK6pYgnB{u=;mA z@AJIRdwt(O@a=VuJ$uf~oGJI*Gjrd+@&2uf+!HJ^EF>hPCkpb?>PSc^{zyp3Cm86! z9ha_W=D-gFTPdlx3Q|&3Z(W_OY#l6-keHGklSGsxO-Um`8eDAKVOXK)&D?Jk$%~X} zS>AE7Ug@rTb3gfsEB7fhP(nir!|}-vhtzmedxHs*gS+5v>hvkMhDlMdcMHhr@FIZH z$;MuXm)-V}`c5T9Hg?;uUjl{+1i0uflYKQsRt@5>UZh|cU!A5=)@1pLS#>u0c!=T) zvx;mwR}A)Fe7JSK8+38%=l)`&HBe#aOUy>G1`pKcxs-2F%`6F5eUl)7=-sdhtKQ4r z{2m9-d)J)vwkg~=LgWXr*m7_AM*dZ6SZjbaTSpz;JS&fMmXBl${I_|G9z$`#{b}mi z4GzTSMNz%f2UNXQa(p>jg*Vk}jGW?wb%TrMZl`^J}t2H6*`r&SmYMwaEETrkYrhY7o>t?8P)OB<3UwR+9Vz zEugkzEOiyEl$DWKfNKmSRAe$FG~fyuI7N}k|9dTq%!Kslulp!SNKv*(sDFo10lptT zNx=CK=Re<%QX-Ka1HTA>)B7XJKcP|lKR){B8ua>EveP>@GB> z5DwhHd?&BxhJ-{+|8OEJsMEoL^nclE>bmPHzY#QdcH}U%a5l5#@OFInkPectw;*uo zXz6ZBgl5T`j5jIXF2uX+*K8sHlWp zEvy99rDgw)4*U|Kv2k~QCkO(0d3kYo@o+f1T7$S=zkUtk^QCX`(Fv%K|2aZfls=N78K(y{`*Z$t51D`yl?j26 z_}@?}!J*YLT_&Owm`O-~A8+BuJmM&H{~TH$kkE{Yf_nUg|4rxP>Tl~{RQZ37FRzfX zI$7>QFTMX8i%z^}5c*GwK+>8nD1`o6H)yBF|BWRd9GY10-wY!Yd|n4~_XU9s`I1AG3MFF)rxgcR-)X$Wgzsw!TE3blEjG^ z#d~!6jX5b3|1AShG0*@1uNd)m)L&l>7%OzDGx+R(VN8M#Rj%u%8JbzYq!8>)aphaV!IC+JaJ zQU19-N;t*cm$L6lSo+$M zD9;s;*75EcKEwOEFcGHT9RQ)ozP+3EBan{{KA5X8?&>5YB&5@%bY=kLK@c@2CdOiC zvIG{wy)-3IX!GR_M?N)=HZ#1_)FxLo{+gUbY+*z`jQJj97mdk_)Jzcow70UBc4e9s{URh}vpgALq1i*sal=JZK6zF5D}F67{Da+2s|sQt)dHdIZW z@MM*qhcH|?_?mZz{xRl5 zv*ZLAgvg^ixgvf|I~qV!&@nPHzP**Q{uL2vwesOCyPd}`%K_^)bNH?Dm!YTZ7s=*L zWK?>Eg{yBcfl59pomC}}CqVYT$N(8|u*PU-$&mdyq{n2dpblYqBi`ZffxW9}+R-cS zbIIv`JsEdl=`zG^ydvKB!PnJwPm0Itb7m-XCh;*jZOETzvn?*4*mP^eS2yh!TWaS5 z>2EvC@PIBD`UWYt7G&5m;eC4gI4Kx+Pjb7_=7;1vZaKleo5tA9o*~ds z@w)BKzHG|@?xo}A`Z=T0lzvYho^lUKpwXKo@oC23HA4*1yu>M>UB28;_)7wkT8y71 z8qik$IEYCSUSfYd@H3rfv+6zcN;KTysWi?pxN+(EBuBNDjLej4UMPrtc8&?NI0^}s z@=iS}3NIuSBytq;HAS3(;BC)iJW76R)u`35S%^pPZw2F!+{%lP^!6DqVNg<0Vkk(@ zIr}V093B7T>PT?rH;xo>am-=^xGCwyl^6x?2Wx165&X~6{7zB#lOuI_t87cGNf3zT^uLcG*wLEf&J4UCDl|Pe|bx?9>*>UwSt-&EHW0UA?7dmZ&bPowK z&KTWWBFtia_>zcs92+>DT_>+4p7*oyTimsVHQJK%K1xN29lOilU#2$4TE|LMr+i$$ zgN4#rg-&OVX`B;MQfMI~O-5e)qcBb zmFneOOL+-HVX9ZfP`#0J`qi9=#**gGD`rH|`kD;EXMI$4cEow7Z~luP9_1finHZF( z8)?+l-`yih`gvgAhgAcUEj zPSUw?F!3ijPJc<~wg1J&X&p9jg2SfagQ8LT=Ba-Z8Ly2=Ad9iaoRHcUpf4>?Hikmo zteaN?{pxFdgssPMDMbzQePGWuj_s>EU_SoOrPms4r``xP+D)gt^qiG=*YuUL$GOLA zI8o=ZXV|8OzHF7uACxJ5Qh6z9++ApzXty~xc4?i%*12w4 z?sxbjB{!Y6F@spCkzzqfVrpO$TdsCh>158jAYBLSBcuIQU#p5Pe5E84%mR#DN+UbS zn}X6(!x;N@BOIlNu^eskDk5h;iyKaV$WdaVl8IV-b|0G~-sJhfvbh4P4%_xPE;loc zEpi>3l7$Xi`l=KkM&U%UyOuZ#k+!&(*}$;-`u5M1B~;{s&5J!7T@1~bp?Wm_oA45= zCo_Vkt*^ z)zr}YPgN_7?DA~JnAQK2i0_}I#}wY;b@lbEY~-rQ{%T-8wh^?x&coRcWYmC5gp1t3`%Zo*ip?vNLO-zId9DO_wQi&+ z75OF~-@_Cb^DM%bfZAf0qwuk48SjrJ85t8;acG~k3(sm;2)=EYQb|RAgl>~uJQ#@8 z>BoSZ@k42#{}ueYJ3Yd+WFK%41< zFK70WmO9dGBs-E&bfF=lg(q?DuQxrhI@4&a9?{{yKq)8oH%@#3D!8wcK>y>wK`t5A z(yBLYG3YsWaxgNe0L2}#>}e_IF8r)Xrd2@;iJOzM!V!KWtGS>LtNNZ!u5w7lY7I>d zC6D|r2H{PwbfSi?W?|Ph935qk5m>iu`%2*T!4SBxMRYXyVCF z%JW**J2^ca!(aTKOuJcLmXGCkhx=+3sdY2MpeXS5;MC(ZU|#iQRp|w?qsIZ+3Kp=k zq7OPdTv`xbc)j(3);iBao8DgGrpcYV#;@Q_k2PM;uL>Pij!uM`?Mi3IF>EY{PieI_ zOjqjF^?v;)-&Ly8sjH<4vtJDH#urOo#=4qj7$hF1q$FEr_vZ`Fu(p^k zIm!S{GCn)PPJwJpF|?K16=Mc^a;4w9foFEpC4QflN>uyCz>78lVi(Zdx_1Wz8pZaX zmWqS6w~i)WcTkR9Dk}0H)LG$y>bev2cWUWPtJi$`hTrq;0Z?oZ*1*)^>p$H_N#pgdNk%+XZY z612(vTyUM$Jw12*?Q-1+n1ob*q-;D%@JKm1;-#HZY?$A0-Sc|O-!bWhPgi#*i~E|m zp2W^IP*=gj-hF#2_kCK&*bz6Nd4#IN<_8Te>egtsIR_xijwoxHl}ReKQaw87>7KC1N-8QU*#q#$k0afv2Y{dgt?MzY!-mZ~NA0Hx?U%6J(yC13z}Gi= zQ6$G{8Y)~Kt9ZoTSBsp1H{Pwc=L_-A^A`gymS)&f(%Owfoh5&0ww_-^tslC@C#?## zPfNJ!)-Q8g%~95QAHc-^w6k2fA2!$d8zr~8!?mwRcD~{!(NkA1Rq%)gf|cavET&1($-)srt-3 zTN#ObF13*arnNjCMk#OXl-B2G%4laMx4eZwh6kR-mPnL|Ne1cRCH?YSWZ;vMVqkT? z5!ZIiV;_UQ`d-Cy$Q$Vj)JTxqii*&I1q?2eE1visE*OW7N7dQg(F-gWHY=(-Q-ba; z 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%0DR=$o(=$rcoP96I(gz)gn$L`o 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

`H^Hbe42~#@wFVHxvC$3ffZj?7ATD{P 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 literal 74421 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

Open in Web Open in Cursor 

GitOrigin-RevId: 2dcb08e87e2c45dba5dde027ded71a13c22f6321 --- go.mod | 4 ++-- go.sum | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 550ffca3..03fde2ef 100644 --- a/go.mod +++ b/go.mod @@ -266,7 +266,7 @@ require ( 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/cloudflare/circl v1.6.3 // 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 @@ -415,7 +415,7 @@ require ( 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/refraction-networking/utls v1.8.2 // 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.31.0 // indirect diff --git a/go.sum b/go.sum index 35d42840..c46a7b11 100644 --- a/go.sum +++ b/go.sum @@ -393,6 +393,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/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= 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= @@ -959,6 +961,8 @@ github.com/qhenkart/anthropic-tokenizer-go v0.0.0-20231011194518-5519949e0faf h1 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/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= +github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= 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= From 011af98b2e415641d7cc89b06eb3098e7ac4d747 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Tue, 3 Mar 2026 15:20:21 +0000 Subject: [PATCH 44/74] Eng 2875 create computecapacityreservation adapter (#4081) image --- > [!NOTE] > **Medium Risk** > Adds a new Azure compute adapter and wires a new Azure SDK client into the central adapter initialization path, which could affect discovery/runtime behavior if query shapes or permissions are incorrect. Changes are otherwise additive and covered by unit tests and mocks. > > **Overview** > Adds a new `ComputeCapacityReservation` searchable adapter that can `Get` a reservation (with `$expand=instanceView`) and `Search` reservations within a capacity reservation group, emitting links to the parent group and associated/allocated VMs and setting health from provisioning state. > > Wires the adapter into `manual/adapters.go` by initializing `armcompute.CapacityReservationsClient` and registering the new wrapper in both real and placeholder adapter lists. > > Introduces a `CapacityReservationsClient` interface + generated GoMock, and updates `shared/utils.go` resource-ID path key mappings for `azure-compute-capacity-reservation`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c1933075c42dc036718ba9950314144bb9be9e9e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 8e771d0b8a12b566319774257fe15b63a4891049 --- .../clients/capacity-reservations-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + .../manual/compute-capacity-reservation.go | 287 +++++++++++++++ .../compute-capacity-reservation_test.go | 346 ++++++++++++++++++ .../mock_capacity_reservations_client.go | 72 ++++ sources/azure/shared/utils.go | 1 + 6 files changed, 751 insertions(+) create mode 100644 sources/azure/clients/capacity-reservations-client.go create mode 100644 sources/azure/manual/compute-capacity-reservation.go create mode 100644 sources/azure/manual/compute-capacity-reservation_test.go create mode 100644 sources/azure/shared/mocks/mock_capacity_reservations_client.go diff --git a/sources/azure/clients/capacity-reservations-client.go b/sources/azure/clients/capacity-reservations-client.go new file mode 100644 index 00000000..cec35b63 --- /dev/null +++ b/sources/azure/clients/capacity-reservations-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_capacity_reservations_client.go -package=mocks -source=capacity-reservations-client.go + +// CapacityReservationsPager is a type alias for the generic Pager interface with capacity reservations list response type. +type CapacityReservationsPager = Pager[armcompute.CapacityReservationsClientListByCapacityReservationGroupResponse] + +// CapacityReservationsClient is an interface for interacting with Azure capacity reservations +type CapacityReservationsClient interface { + NewListByCapacityReservationGroupPager(resourceGroupName string, capacityReservationGroupName string, options *armcompute.CapacityReservationsClientListByCapacityReservationGroupOptions) CapacityReservationsPager + Get(ctx context.Context, resourceGroupName string, capacityReservationGroupName string, capacityReservationName string, options *armcompute.CapacityReservationsClientGetOptions) (armcompute.CapacityReservationsClientGetResponse, error) +} + +type capacityReservationsClient struct { + client *armcompute.CapacityReservationsClient +} + +func (c *capacityReservationsClient) NewListByCapacityReservationGroupPager(resourceGroupName string, capacityReservationGroupName string, options *armcompute.CapacityReservationsClientListByCapacityReservationGroupOptions) CapacityReservationsPager { + return c.client.NewListByCapacityReservationGroupPager(resourceGroupName, capacityReservationGroupName, options) +} + +func (c *capacityReservationsClient) Get(ctx context.Context, resourceGroupName string, capacityReservationGroupName string, capacityReservationName string, options *armcompute.CapacityReservationsClientGetOptions) (armcompute.CapacityReservationsClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, capacityReservationGroupName, capacityReservationName, options) +} + +// NewCapacityReservationsClient creates a new CapacityReservationsClient from the Azure SDK client +func NewCapacityReservationsClient(client *armcompute.CapacityReservationsClient) CapacityReservationsClient { + return &capacityReservationsClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index cfd91fc4..5794734d 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -333,6 +333,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create capacity reservation groups client: %w", err) } + capacityReservationsClient, err := armcompute.NewCapacityReservationsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create capacity reservations 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) @@ -574,6 +579,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewCapacityReservationGroupsClient(capacityReservationGroupsClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewComputeCapacityReservation( + clients.NewCapacityReservationsClient(capacityReservationsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewComputeGalleryApplicationVersion( clients.NewGalleryApplicationVersionsClient(galleryApplicationVersionsClient), resourceGroupScopes, @@ -666,6 +675,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewComputeDedicatedHostGroup(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeDedicatedHost(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeCapacityReservationGroup(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewComputeCapacityReservation(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeGalleryApplicationVersion(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeGalleryApplication(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeGallery(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/compute-capacity-reservation.go b/sources/azure/manual/compute-capacity-reservation.go new file mode 100644 index 00000000..697853bb --- /dev/null +++ b/sources/azure/manual/compute-capacity-reservation.go @@ -0,0 +1,287 @@ +package manual + +import ( + "context" + "errors" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +var ComputeCapacityReservationLookupByName = shared.NewItemTypeLookup("name", azureshared.ComputeCapacityReservation) + +type computeCapacityReservationWrapper struct { + client clients.CapacityReservationsClient + *azureshared.MultiResourceGroupBase +} + +func NewComputeCapacityReservation(client clients.CapacityReservationsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &computeCapacityReservationWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_COMPUTE_APPLICATION, + azureshared.ComputeCapacityReservation, + ), + } +} + +func capacityReservationGetOptions() *armcompute.CapacityReservationsClientGetOptions { + expand := armcompute.CapacityReservationInstanceViewTypesInstanceView + return &armcompute.CapacityReservationsClientGetOptions{ + Expand: &expand, + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/capacity-reservations/get?view=rest-compute-2025-04-01&tabs=HTTP +func (c *computeCapacityReservationWrapper) 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: capacity reservation group name and capacity reservation name"), scope, c.Type()) + } + groupName := queryParts[0] + if groupName == "" { + return nil, azureshared.QueryError(errors.New("capacity reservation group name cannot be empty"), scope, c.Type()) + } + reservationName := queryParts[1] + if reservationName == "" { + return nil, azureshared.QueryError(errors.New("capacity reservation 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, groupName, reservationName, capacityReservationGetOptions()) + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + return c.azureCapacityReservationToSDPItem(&resp.CapacityReservation, groupName, scope) +} + +// ref: https://learn.microsoft.com/en-us/rest/api/compute/capacity-reservations/list-by-capacity-reservation-group?view=rest-compute-2025-04-01&tabs=HTTP +func (c *computeCapacityReservationWrapper) 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: capacity reservation group name"), scope, c.Type()) + } + groupName := queryParts[0] + if groupName == "" { + 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()) + } + pager := c.client.NewListByCapacityReservationGroupPager(rgScope.ResourceGroup, groupName, 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 _, res := range page.Value { + if res == nil || res.Name == nil { + continue + } + item, sdpErr := c.azureCapacityReservationToSDPItem(res, groupName, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (c *computeCapacityReservationWrapper) 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: capacity reservation group name"), scope, c.Type())) + return + } + groupName := queryParts[0] + if groupName == "" { + stream.SendError(azureshared.QueryError(errors.New("capacity reservation group 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.NewListByCapacityReservationGroupPager(rgScope.ResourceGroup, groupName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, c.Type())) + return + } + for _, res := range page.Value { + if res == nil || res.Name == nil { + continue + } + item, sdpErr := c.azureCapacityReservationToSDPItem(res, groupName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (c *computeCapacityReservationWrapper) azureCapacityReservationToSDPItem(res *armcompute.CapacityReservation, groupName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(res, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + if res.Name == nil { + return nil, azureshared.QueryError(errors.New("capacity reservation name is nil"), scope, c.Type()) + } + reservationName := *res.Name + if reservationName == "" { + return nil, azureshared.QueryError(errors.New("capacity reservation name cannot be empty"), scope, c.Type()) + } + if err := attributes.Set("uniqueAttr", shared.CompositeLookupKey(groupName, reservationName)); err != nil { + return nil, azureshared.QueryError(err, scope, c.Type()) + } + + linkedItemQueries := make([]*sdp.LinkedItemQuery, 0) + + // Parent: capacity reservation group + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeCapacityReservationGroup.String(), + Method: sdp.QueryMethod_GET, + Query: groupName, + Scope: scope, + }, + }) + + // VMs associated with this capacity reservation + if res.Properties != nil && res.Properties.VirtualMachinesAssociated != nil { + for _, vmRef := range res.Properties.VirtualMachinesAssociated { + if vmRef == nil || vmRef.ID == nil || *vmRef.ID == "" { + continue + } + vmName := azureshared.ExtractResourceName(*vmRef.ID) + if vmName == "" { + continue + } + vmScope := scope + if linkScope := azureshared.ExtractScopeFromResourceID(*vmRef.ID); linkScope != "" { + vmScope = linkScope + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeVirtualMachine.String(), + Method: sdp.QueryMethod_GET, + Query: vmName, + Scope: vmScope, + }, + }) + } + } + + // VMs physically allocated to this capacity reservation (from instance view; only populated when Get uses $expand=instanceView) + if res.Properties != nil && res.Properties.InstanceView != nil && res.Properties.InstanceView.UtilizationInfo != nil && res.Properties.InstanceView.UtilizationInfo.VirtualMachinesAllocated != nil { + for _, vmRef := range res.Properties.InstanceView.UtilizationInfo.VirtualMachinesAllocated { + if vmRef == nil || vmRef.ID == nil || *vmRef.ID == "" { + continue + } + vmName := azureshared.ExtractResourceName(*vmRef.ID) + if vmName == "" { + continue + } + vmScope := scope + if linkScope := azureshared.ExtractScopeFromResourceID(*vmRef.ID); linkScope != "" { + vmScope = linkScope + } + linkedItemQueries = append(linkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ComputeVirtualMachine.String(), + Method: sdp.QueryMethod_GET, + Query: vmName, + Scope: vmScope, + }, + }) + } + } + + sdpItem := &sdp.Item{ + Type: azureshared.ComputeCapacityReservation.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(res.Tags), + LinkedItemQueries: linkedItemQueries, + } + + // Health status from ProvisioningState + if res.Properties != nil && res.Properties.ProvisioningState != nil { + state := strings.ToLower(*res.Properties.ProvisioningState) + switch state { + 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() + } + } + + return sdpItem, nil +} + +func (c *computeCapacityReservationWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + ComputeCapacityReservationGroupLookupByName, + ComputeCapacityReservationLookupByName, + } +} + +func (c *computeCapacityReservationWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + ComputeCapacityReservationGroupLookupByName, + }, + } +} + +func (c *computeCapacityReservationWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.ComputeCapacityReservationGroup: true, + azureshared.ComputeVirtualMachine: true, + } +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/capacity_reservation +func (c *computeCapacityReservationWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_SEARCH, + TerraformQueryMap: "azurerm_capacity_reservation.id", + }, + } +} + +func (c *computeCapacityReservationWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Compute/capacityReservationGroups/capacityReservations/read", + } +} + +func (c *computeCapacityReservationWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/compute-capacity-reservation_test.go b/sources/azure/manual/compute-capacity-reservation_test.go new file mode 100644 index 00000000..515e7285 --- /dev/null +++ b/sources/azure/manual/compute-capacity-reservation_test.go @@ -0,0 +1,346 @@ +package manual + +import ( + "context" + "errors" + "testing" + + "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/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" +) + +func createAzureCapacityReservation(reservationName, groupName string) *armcompute.CapacityReservation { + return &armcompute.CapacityReservation{ + ID: new("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/capacityReservationGroups/" + groupName + "/capacityReservations/" + reservationName), + Name: new(reservationName), + Type: new("Microsoft.Compute/capacityReservationGroups/capacityReservations"), + Location: new("eastus"), + Tags: map[string]*string{"env": new("test")}, + SKU: &armcompute.SKU{ + Name: new("Standard_D2s_v3"), + Capacity: new(int64(1)), + }, + Properties: &armcompute.CapacityReservationProperties{ + ProvisioningState: new("Succeeded"), + }, + } +} + +func createAzureCapacityReservationWithVMs(reservationName, groupName, subscriptionID, resourceGroup string, vmNames ...string) *armcompute.CapacityReservation { + vms := make([]*armcompute.SubResourceReadOnly, 0, len(vmNames)) + for _, vmName := range vmNames { + vms = append(vms, &armcompute.SubResourceReadOnly{ + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Compute/virtualMachines/" + vmName), + }) + } + return &armcompute.CapacityReservation{ + ID: new("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/capacityReservationGroups/" + groupName + "/capacityReservations/" + reservationName), + Name: new(reservationName), + Type: new("Microsoft.Compute/capacityReservationGroups/capacityReservations"), + Location: new("eastus"), + Tags: map[string]*string{"env": new("test")}, + SKU: &armcompute.SKU{ + Name: new("Standard_D2s_v3"), + Capacity: new(int64(1)), + }, + Properties: &armcompute.CapacityReservationProperties{ + ProvisioningState: new("Succeeded"), + VirtualMachinesAssociated: vms, + }, + } +} + +type mockCapacityReservationsPager struct { + items []*armcompute.CapacityReservation + index int +} + +func (m *mockCapacityReservationsPager) More() bool { + return m.index < len(m.items) +} + +func (m *mockCapacityReservationsPager) NextPage(ctx context.Context) (armcompute.CapacityReservationsClientListByCapacityReservationGroupResponse, error) { + if m.index >= len(m.items) { + return armcompute.CapacityReservationsClientListByCapacityReservationGroupResponse{ + CapacityReservationListResult: armcompute.CapacityReservationListResult{ + Value: []*armcompute.CapacityReservation{}, + }, + }, nil + } + item := m.items[m.index] + m.index++ + return armcompute.CapacityReservationsClientListByCapacityReservationGroupResponse{ + CapacityReservationListResult: armcompute.CapacityReservationListResult{ + Value: []*armcompute.CapacityReservation{item}, + }, + }, nil +} + +type errorCapacityReservationsPager struct{} + +func (e *errorCapacityReservationsPager) More() bool { + return true +} + +func (e *errorCapacityReservationsPager) NextPage(ctx context.Context) (armcompute.CapacityReservationsClientListByCapacityReservationGroupResponse, error) { + return armcompute.CapacityReservationsClientListByCapacityReservationGroupResponse{}, errors.New("pager error") +} + +type testCapacityReservationsClient struct { + *mocks.MockCapacityReservationsClient + pager clients.CapacityReservationsPager +} + +func (t *testCapacityReservationsClient) NewListByCapacityReservationGroupPager(resourceGroupName string, capacityReservationGroupName string, options *armcompute.CapacityReservationsClientListByCapacityReservationGroupOptions) clients.CapacityReservationsPager { + if t.pager != nil { + return t.pager + } + return t.MockCapacityReservationsClient.NewListByCapacityReservationGroupPager(resourceGroupName, capacityReservationGroupName, options) +} + +func TestComputeCapacityReservation(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + scope := subscriptionID + "." + resourceGroup + groupName := "test-crg" + reservationName := "test-reservation" + + t.Run("Get", func(t *testing.T) { + res := createAzureCapacityReservation(reservationName, groupName) + + mockClient := mocks.NewMockCapacityReservationsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, groupName, reservationName, gomock.Eq(capacityReservationGetOptions())).Return( + armcompute.CapacityReservationsClientGetResponse{ + CapacityReservation: *res, + }, nil) + + wrapper := NewComputeCapacityReservation(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(groupName, reservationName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.ComputeCapacityReservation.String() { + t.Errorf("Expected type %s, got %s", azureshared.ComputeCapacityReservation.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUnique := shared.CompositeLookupKey(groupName, reservationName) + 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.ComputeCapacityReservationGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: groupName, ExpectedScope: scope}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_WithVMLinks", func(t *testing.T) { + res := createAzureCapacityReservationWithVMs(reservationName, groupName, subscriptionID, resourceGroup, "vm-1", "vm-2") + + mockClient := mocks.NewMockCapacityReservationsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, groupName, reservationName, gomock.Eq(capacityReservationGetOptions())).Return( + armcompute.CapacityReservationsClientGetResponse{ + CapacityReservation: *res, + }, nil) + + wrapper := NewComputeCapacityReservation(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(groupName, reservationName) + sdpItem, qErr := adapter.Get(ctx, scope, query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + queryTests := shared.QueryTests{ + {ExpectedType: azureshared.ComputeCapacityReservationGroup.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: groupName, ExpectedScope: scope}, + {ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "vm-1", ExpectedScope: scope}, + {ExpectedType: azureshared.ComputeVirtualMachine.String(), ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "vm-2", ExpectedScope: scope}, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + + t.Run("Get_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationsClient(ctrl) + wrapper := NewComputeCapacityReservation(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, groupName, true) + if qErr == nil { + t.Error("Expected error when Get with wrong number of query parts, but got nil") + } + }) + + t.Run("Get_EmptyGroupName", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationsClient(ctrl) + wrapper := NewComputeCapacityReservation(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey("", reservationName) + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when capacity reservation group name is empty, but got nil") + } + }) + + t.Run("Get_EmptyReservationName", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationsClient(ctrl) + wrapper := NewComputeCapacityReservation(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(groupName, "") + _, qErr := adapter.Get(ctx, scope, query, true) + if qErr == nil { + t.Error("Expected error when capacity reservation name is empty, but got nil") + } + }) + + t.Run("Get_ClientError", func(t *testing.T) { + expectedErr := errors.New("reservation not found") + mockClient := mocks.NewMockCapacityReservationsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, groupName, "nonexistent", gomock.Eq(capacityReservationGetOptions())).Return( + armcompute.CapacityReservationsClientGetResponse{}, expectedErr) + + wrapper := NewComputeCapacityReservation(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(groupName, "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) { + res1 := createAzureCapacityReservation("res-1", groupName) + res2 := createAzureCapacityReservation("res-2", groupName) + + mockClient := mocks.NewMockCapacityReservationsClient(ctrl) + pager := &mockCapacityReservationsPager{ + items: []*armcompute.CapacityReservation{res1, res2}, + } + testClient := &testCapacityReservationsClient{ + MockCapacityReservationsClient: mockClient, + pager: pager, + } + + wrapper := NewComputeCapacityReservation(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, groupName, 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.NewMockCapacityReservationsClient(ctrl) + wrapper := NewComputeCapacityReservation(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, scope, groupName, reservationName) + if qErr == nil { + t.Error("Expected error when Search with wrong number of query parts, but got nil") + } + }) + + t.Run("Search_EmptyGroupName", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationsClient(ctrl) + wrapper := NewComputeCapacityReservation(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, scope, "") + if qErr == nil { + t.Error("Expected error when capacity reservation group name is empty, but got nil") + } + }) + + t.Run("Search_PagerError", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationsClient(ctrl) + errorPager := &errorCapacityReservationsPager{} + testClient := &testCapacityReservationsClient{ + MockCapacityReservationsClient: mockClient, + pager: errorPager, + } + + wrapper := NewComputeCapacityReservation(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, groupName, true) + if err == nil { + t.Error("Expected error when pager returns error, but got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := mocks.NewMockCapacityReservationsClient(ctrl) + wrapper := NewComputeCapacityReservation(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + links := wrapper.PotentialLinks() + expected := map[shared.ItemType]bool{ + azureshared.ComputeCapacityReservationGroup: true, + azureshared.ComputeVirtualMachine: 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.NewMockCapacityReservationsClient(ctrl) + wrapper := NewComputeCapacityReservation(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/shared/mocks/mock_capacity_reservations_client.go b/sources/azure/shared/mocks/mock_capacity_reservations_client.go new file mode 100644 index 00000000..cf65eb9c --- /dev/null +++ b/sources/azure/shared/mocks/mock_capacity_reservations_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: capacity-reservations-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_capacity_reservations_client.go -package=mocks -source=capacity-reservations-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" +) + +// MockCapacityReservationsClient is a mock of CapacityReservationsClient interface. +type MockCapacityReservationsClient struct { + ctrl *gomock.Controller + recorder *MockCapacityReservationsClientMockRecorder + isgomock struct{} +} + +// MockCapacityReservationsClientMockRecorder is the mock recorder for MockCapacityReservationsClient. +type MockCapacityReservationsClientMockRecorder struct { + mock *MockCapacityReservationsClient +} + +// NewMockCapacityReservationsClient creates a new mock instance. +func NewMockCapacityReservationsClient(ctrl *gomock.Controller) *MockCapacityReservationsClient { + mock := &MockCapacityReservationsClient{ctrl: ctrl} + mock.recorder = &MockCapacityReservationsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockCapacityReservationsClient) EXPECT() *MockCapacityReservationsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockCapacityReservationsClient) Get(ctx context.Context, resourceGroupName, capacityReservationGroupName, capacityReservationName string, options *armcompute.CapacityReservationsClientGetOptions) (armcompute.CapacityReservationsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, capacityReservationGroupName, capacityReservationName, options) + ret0, _ := ret[0].(armcompute.CapacityReservationsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockCapacityReservationsClientMockRecorder) Get(ctx, resourceGroupName, capacityReservationGroupName, capacityReservationName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockCapacityReservationsClient)(nil).Get), ctx, resourceGroupName, capacityReservationGroupName, capacityReservationName, options) +} + +// NewListByCapacityReservationGroupPager mocks base method. +func (m *MockCapacityReservationsClient) NewListByCapacityReservationGroupPager(resourceGroupName, capacityReservationGroupName string, options *armcompute.CapacityReservationsClientListByCapacityReservationGroupOptions) clients.CapacityReservationsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByCapacityReservationGroupPager", resourceGroupName, capacityReservationGroupName, options) + ret0, _ := ret[0].(clients.CapacityReservationsPager) + return ret0 +} + +// NewListByCapacityReservationGroupPager indicates an expected call of NewListByCapacityReservationGroupPager. +func (mr *MockCapacityReservationsClientMockRecorder) NewListByCapacityReservationGroupPager(resourceGroupName, capacityReservationGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByCapacityReservationGroupPager", reflect.TypeOf((*MockCapacityReservationsClient)(nil).NewListByCapacityReservationGroupPager), resourceGroupName, capacityReservationGroupName, options) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 2880bf50..bcc1dd78 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -40,6 +40,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-compute-gallery-application": {"galleries", "applications"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/applications/{applicationName}", "azure-compute-gallery-image": {"galleries", "images"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/galleries/{galleryName}/images/{imageName}", "azure-compute-dedicated-host": {"hostGroups", "hosts"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/hostGroups/{hostGroupName}/hosts/{hostName}", + "azure-compute-capacity-reservation": {"capacityReservationGroups", "capacityReservations"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Compute/capacityReservationGroups/{groupName}/capacityReservations/{reservationName}", "azure-network-subnet": {"virtualNetworks", "subnets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/subnets/{subnetName}", "azure-network-virtual-network-peering": {"virtualNetworks", "virtualNetworkPeerings"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/virtualNetworks/{vnetName}/virtualNetworkPeerings/{peeringName}", "azure-network-route": {"routeTables", "routes"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/routeTables/{routeTableName}/routes/{routeName}", From dbb09da99a9b2b5c92d7167a8e29ce453256c759 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Tue, 3 Mar 2026 17:00:16 +0100 Subject: [PATCH 45/74] GitHub Actions script injection (#4088) Fix GitHub Actions script injection by using `env:` mapping instead of direct interpolation for workflow variables. Direct interpolation of `${{ }}` expressions inside `run:` shell blocks is a well-documented GitHub Actions script injection vector, allowing arbitrary command execution with crafted branch names. Using `env:` mapping passes the value as an environment variable, preventing shell metacharacter interpretation. --- Linear Issue: [ENG-2675](https://linear.app/overmind/issue/ENG-2675/fix-github-actions-script-injection-in-copybara-workflow-files)

Open in Web Open in Cursor 

Co-authored-by: Cursor Agent Co-authored-by: David Schmitt GitOrigin-RevId: f8845d5e8f37082427fe21e375d379dc5c3d6484 --- .github/workflows/finalize-copybara-sync.yml | 7 +++++-- .github/workflows/tag-on-merge.yml | 5 +++-- .../provider/.github/workflows/finalize-copybara-sync.yml | 7 +++++-- .../module/provider/.github/workflows/tag-on-merge.yml | 5 +++-- .../terraform/.github/workflows/finalize-copybara-sync.yml | 3 ++- .../module/terraform/.github/workflows/tag-on-merge.yml | 5 +++-- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/.github/workflows/finalize-copybara-sync.yml b/.github/workflows/finalize-copybara-sync.yml index 62426bd6..070d3ff7 100644 --- a/.github/workflows/finalize-copybara-sync.yml +++ b/.github/workflows/finalize-copybara-sync.yml @@ -45,11 +45,13 @@ jobs: run: go mod tidy - name: Commit and push go mod tidy changes + env: + HEAD_BRANCH: ${{ github.ref_name }} 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 }} + git push origin "$HEAD_BRANCH" else echo "No changes from go mod tidy" fi @@ -81,6 +83,7 @@ jobs: AUTHOR_NAME: ${{ steps.author.outputs.name }} AUTHOR_EMAIL: ${{ steps.author.outputs.email }} GITHUB_USER: ${{ steps.author.outputs.github_user }} + HEAD_BRANCH: ${{ github.ref_name }} run: | # Build PR body PR_BODY="## Copybara Sync - Release ${VERSION} @@ -106,7 +109,7 @@ jobs: # Create the PR PR_URL=$(gh pr create \ --base main \ - --head "${{ github.ref_name }}" \ + --head "$HEAD_BRANCH" \ --title "Release ${VERSION}" \ --body "$PR_BODY") diff --git a/.github/workflows/tag-on-merge.yml b/.github/workflows/tag-on-merge.yml index 541619db..b2192dab 100644 --- a/.github/workflows/tag-on-merge.yml +++ b/.github/workflows/tag-on-merge.yml @@ -18,9 +18,10 @@ jobs: steps: - name: Extract version from branch name id: version + env: + BRANCH: ${{ github.event.pull_request.head.ref }} run: | # Extract v1.2.3 from copybara/v1.2.3 - BRANCH="${{ github.event.pull_request.head.ref }}" VERSION=$(echo "$BRANCH" | sed 's|copybara/||') echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Extracted version: $VERSION" @@ -48,8 +49,8 @@ jobs: - name: Delete copybara branch env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: ${{ github.event.pull_request.head.ref }} 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/.github/workflows/finalize-copybara-sync.yml b/aws-source/module/provider/.github/workflows/finalize-copybara-sync.yml index 3a59a73a..f4bab3a0 100644 --- a/aws-source/module/provider/.github/workflows/finalize-copybara-sync.yml +++ b/aws-source/module/provider/.github/workflows/finalize-copybara-sync.yml @@ -42,11 +42,13 @@ jobs: run: go mod tidy - name: Commit and push go mod tidy changes + env: + HEAD_BRANCH: ${{ github.ref_name }} 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 }} + git push origin "$HEAD_BRANCH" else echo "No changes from go mod tidy" fi @@ -73,6 +75,7 @@ jobs: AUTHOR_NAME: ${{ steps.author.outputs.name }} AUTHOR_EMAIL: ${{ steps.author.outputs.email }} GITHUB_USER: ${{ steps.author.outputs.github_user }} + HEAD_BRANCH: ${{ github.ref_name }} run: | PR_BODY="## Copybara Sync - Release ${VERSION} @@ -97,7 +100,7 @@ jobs: PR_URL=$(gh pr create \ --base main \ - --head "${{ github.ref_name }}" \ + --head "$HEAD_BRANCH" \ --title "Release ${VERSION}" \ --body "$PR_BODY") diff --git a/aws-source/module/provider/.github/workflows/tag-on-merge.yml b/aws-source/module/provider/.github/workflows/tag-on-merge.yml index 800ccb96..44dc9c22 100644 --- a/aws-source/module/provider/.github/workflows/tag-on-merge.yml +++ b/aws-source/module/provider/.github/workflows/tag-on-merge.yml @@ -17,8 +17,9 @@ jobs: steps: - name: Extract version from branch name id: version + env: + BRANCH: ${{ github.event.pull_request.head.ref }} run: | - BRANCH="${{ github.event.pull_request.head.ref }}" VERSION=$(echo "$BRANCH" | sed 's|copybara/||') echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Extracted version: $VERSION" @@ -46,7 +47,7 @@ jobs: - name: Delete copybara branch env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: ${{ github.event.pull_request.head.ref }} run: | - BRANCH="${{ github.event.pull_request.head.ref }}" echo "Deleting branch: $BRANCH" git push origin --delete "$BRANCH" || echo "Branch may have already been deleted" diff --git a/aws-source/module/terraform/.github/workflows/finalize-copybara-sync.yml b/aws-source/module/terraform/.github/workflows/finalize-copybara-sync.yml index e6c0f5e0..f0d30412 100644 --- a/aws-source/module/terraform/.github/workflows/finalize-copybara-sync.yml +++ b/aws-source/module/terraform/.github/workflows/finalize-copybara-sync.yml @@ -50,6 +50,7 @@ jobs: AUTHOR_NAME: ${{ steps.author.outputs.name }} AUTHOR_EMAIL: ${{ steps.author.outputs.email }} GITHUB_USER: ${{ steps.author.outputs.github_user }} + HEAD_BRANCH: ${{ github.ref_name }} run: | PR_BODY="## Copybara Sync - Release ${VERSION} @@ -69,7 +70,7 @@ jobs: PR_URL=$(gh pr create \ --base main \ - --head "${{ github.ref_name }}" \ + --head "$HEAD_BRANCH" \ --title "Release ${VERSION}" \ --body "$PR_BODY") diff --git a/aws-source/module/terraform/.github/workflows/tag-on-merge.yml b/aws-source/module/terraform/.github/workflows/tag-on-merge.yml index 800ccb96..44dc9c22 100644 --- a/aws-source/module/terraform/.github/workflows/tag-on-merge.yml +++ b/aws-source/module/terraform/.github/workflows/tag-on-merge.yml @@ -17,8 +17,9 @@ jobs: steps: - name: Extract version from branch name id: version + env: + BRANCH: ${{ github.event.pull_request.head.ref }} run: | - BRANCH="${{ github.event.pull_request.head.ref }}" VERSION=$(echo "$BRANCH" | sed 's|copybara/||') echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Extracted version: $VERSION" @@ -46,7 +47,7 @@ jobs: - name: Delete copybara branch env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BRANCH: ${{ github.event.pull_request.head.ref }} 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 04221794793da2bfe61bebdbaede2e9f734ccdcc Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Tue, 3 Mar 2026 17:46:09 +0100 Subject: [PATCH 46/74] [ENG-2935] Add tracing instrumentation for Source WaitGroups stuck diagnosis (#4089) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add instrumentation to `go/discovery/enginerequests.go` and `go/discovery/querytracker.go` to confirm or reject hypotheses H2–H5 for the "Source WaitGroups stuck" Honeycomb trigger - Capture goroutine profiles (pprof debug=1) automatically when the stuck condition fires, with big-picture query counts to immediately distinguish few-stuck-adapters from systemic backpressure - All changes are additive span attributes and span events — no logic changes ## Linear Ticket - **Ticket**: [ENG-2935](https://linear.app/overmind/issue/ENG-2935/add-tracing-instrumentation-for-source-waitgroups-stuck-diagnosis) — Add tracing instrumentation for Source WaitGroups stuck diagnosis - **Purpose**: Add observability to diagnose the root cause of the "Source WaitGroups stuck" trigger in production - **Related**: [ENG-722](https://linear.app/overmind/issue/ENG-722) — Very slow change analysis - gateway QueryProgress ## Changes ### New instrumentation (all additive, no logic changes) | Attribute | Span | Purpose | | --- | --- | --- | | `ovm.discovery.poolWaitMs` | HandleQuery | Detect pool saturation (H3) | | `ovm.discovery.mutexWaitMs` + `mutexKey` | Execute | Detect GET/LIST mutex convoy effects (H2) | | `ovm.discovery.channelSendMaxMs` + `TotalMs` | Execute | Detect response channel backpressure (H5) | | `ovm.nats.publishMaxMs` + `TotalMs` + `Count` | HandleQuery | Identify NATS as root cause of backpressure (H5) | | `ovm.stuck.*` (goroutineCount, goroutineProfile, totalQueries, remainingQueries, adapter, type, scope, method) | HandleQuery events | Goroutine profile + query breakdown when stuck fires | ### Files changed - `go/discovery/enginerequests.go` — Parts 1, 2, 3, 5 (pool wait, mutex wait, channel send stats, goroutine capture) - `go/discovery/querytracker.go` — Part 4 (NATS publish latency) - `docs/plans/source-waitgroup-tracing-improvements.md` — Implementation plan - `docs/plans/source-waitgroups-stuck-analysis.md` — Root cause analysis report ### Review focus - The `captureGoroutineSummary` helper truncates to 48 KB to stay within Honeycomb's 49 KB string attribute limit - Channel send timing uses atomic CAS for the max to safely aggregate across concurrent item/error handlers - NATS publish timing uses plain local variables (single-goroutine loop in `QueryTracker.Execute`) ## Deviations from Approved Plan Implementation matches the approved plan — no material deviations. Made with [Cursor](https://cursor.com) --- > [!NOTE] > **Medium Risk** > Mostly additive tracing, but it instruments hot paths (pool submission, mutex locking, channel sends, NATS publish) and captures goroutine profiles on timeout; this could add overhead and increased span/event size under high load. > > **Overview** > Adds **new tracing instrumentation** to diagnose the production "Source WaitGroups stuck" condition. > > In `enginerequests.go`, it records worst-case pool admission delay (`ovm.discovery.poolWaitMaxMs`), mutex acquisition wait + key (`ovm.discovery.mutexWaitMs`, `ovm.discovery.mutexKey`), and aggregate response-channel send blocking (`ovm.discovery.channelSendMaxMs/TotalMs`). When the 2-minute stuck timeout fires, it now emits `waitgroup.stuck` span events including a truncated aggregated goroutine pprof summary plus total vs remaining query counts and per-adapter stuck details. > > In `querytracker.go`, it times and aggregates NATS publish latency per query (`ovm.nats.publishMaxMs/TotalMs/Count`). Also includes minor cleanup removing obsolete `nolint` annotations and adding docs planning/analysis writeups. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2d1884e4828b0df87c3a166c16c0ad1bfa02c7c1. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 3da1dfb83aa6466b009929f897ee436754427242 --- go/discovery/enginerequests.go | 80 ++++++++++++++++++++++++++++++++ go/discovery/querytracker.go | 18 +++++++ k8s-source/adapters/endpoints.go | 10 ++-- 3 files changed, 103 insertions(+), 5 deletions(-) diff --git a/go/discovery/enginerequests.go b/go/discovery/enginerequests.go index 3b04bf82..6592e8af 100644 --- a/go/discovery/enginerequests.go +++ b/go/discovery/enginerequests.go @@ -1,9 +1,12 @@ package discovery import ( + "bytes" "context" "errors" "fmt" + "runtime" + "runtime/pprof" "sync" "sync/atomic" "time" @@ -154,6 +157,20 @@ func (e *Engine) HandleQuery(ctx context.Context, query *sdp.Query) { } } +// captureGoroutineSummary returns an aggregated goroutine profile (pprof +// debug=1 format) truncated to maxBytes. The debug=1 format groups goroutines +// by unique stack trace, keeping output compact enough for a Honeycomb string +// attribute (49 KB limit). +func captureGoroutineSummary(maxBytes int) string { + var buf bytes.Buffer + _ = pprof.Lookup("goroutine").WriteTo(&buf, 1) + s := buf.String() + if len(s) > maxBytes { + s = s[:maxBytes-20] + "\n...[truncated]..." + } + return s +} + var listExecutionPoolCount atomic.Int32 var getExecutionPoolCount atomic.Int32 @@ -196,6 +213,8 @@ func (e *Engine) ExecuteQuery(ctx context.Context, query *sdp.Query, responses c // Overall MaxParallelExecutions evaluation is handled by e.executionPool wg := sync.WaitGroup{} expandedMutex := sync.RWMutex{} + totalQueries := len(expanded) + var poolWaitMaxNs atomic.Int64 expandedMutex.RLock() for q, adapter := range expanded { wg.Add(1) @@ -220,8 +239,16 @@ func (e *Engine) ExecuteQuery(ctx context.Context, query *sdp.Query, responses c attribute.Int("ovm.discovery.listExecutionPoolCount", int(listExecutionPoolCount.Load())), attribute.Int("ovm.discovery.getExecutionPoolCount", int(getExecutionPoolCount.Load())), ) + poolSubmitTime := time.Now() p.Go(func() { defer tracing.LogRecoverToReturn(ctx, "ExecuteQuery inner") + waitNs := time.Since(poolSubmitTime).Nanoseconds() + for { + old := poolWaitMaxNs.Load() + if waitNs <= old || poolWaitMaxNs.CompareAndSwap(old, waitNs) { + break + } + } defer func() { // Mark the work as done. This happens before we start // waiting on `expandedMutex` below, to ensure that the @@ -284,8 +311,21 @@ func (e *Engine) ExecuteQuery(ctx context.Context, query *sdp.Query, responses c return case <-time.After(longRunningAdaptersTimeout): // If we're here, then the wait group didn't finish in time + goroutineSummary := captureGoroutineSummary(48_000) expandedMutex.RLock() + span.AddEvent("waitgroup.stuck", trace.WithAttributes( + attribute.Int("ovm.stuck.goroutineCount", runtime.NumGoroutine()), + attribute.String("ovm.stuck.goroutineProfile", goroutineSummary), + attribute.Int("ovm.stuck.totalQueries", totalQueries), + attribute.Int("ovm.stuck.remainingQueries", len(expanded)), + )) for q, adapter := range expanded { + span.AddEvent("waitgroup.stuck.adapter", trace.WithAttributes( + attribute.String("ovm.stuck.adapter", adapter.Name()), + attribute.String("ovm.stuck.type", q.GetType()), + attribute.String("ovm.stuck.scope", q.GetScope()), + attribute.String("ovm.stuck.method", q.GetMethod().String()), + )) // There is a honeycomb trigger for this message: // // https://ui.honeycomb.io/overmind/environments/prod/datasets/kubernetes-metrics/triggers/saWNAnCAXNb @@ -310,6 +350,10 @@ func (e *Engine) ExecuteQuery(ctx context.Context, query *sdp.Query, responses c }() } + span.SetAttributes( + attribute.Float64("ovm.discovery.poolWaitMaxMs", float64(poolWaitMaxNs.Load())/1e6), + ) + // If the context is cancelled, return that error if ctx.Err() != nil { return ctx.Err() @@ -344,6 +388,7 @@ func (e *Engine) Execute(ctx context.Context, q *sdp.Query, adapter Adapter, res // 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 + mutexWaitStart := time.Now() switch q.GetMethod() { case sdp.QueryMethod_GET: e.gfm.GetLock(q.GetScope(), q.GetType()) @@ -355,6 +400,10 @@ func (e *Engine) Execute(ctx context.Context, q *sdp.Query, adapter Adapter, res // 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 } + span.SetAttributes( + attribute.Float64("ovm.discovery.mutexWaitMs", float64(time.Since(mutexWaitStart).Milliseconds())), + attribute.String("ovm.discovery.mutexKey", q.GetScope()+"."+q.GetType()), + ) // 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 @@ -377,6 +426,8 @@ func (e *Engine) Execute(ctx context.Context, q *sdp.Query, adapter Adapter, res // are passed back to the caller var numItems atomic.Int32 var numErrs atomic.Int32 + var channelSendMaxNs atomic.Int64 + var channelSendTotalNs atomic.Int64 var itemHandler ItemHandler = func(item *sdp.Item) { if item == nil { return @@ -384,6 +435,7 @@ func (e *Engine) Execute(ctx context.Context, q *sdp.Query, adapter Adapter, res if err := item.Validate(); err != nil { span.RecordError(err) + sendStart := time.Now() responses <- sdp.NewQueryResponseFromError(&sdp.QueryError{ UUID: q.GetUUID(), ErrorType: sdp.QueryError_OTHER, @@ -392,6 +444,14 @@ func (e *Engine) Execute(ctx context.Context, q *sdp.Query, adapter Adapter, res ResponderName: e.EngineConfig.SourceName, ItemType: q.GetType(), }) + sendNs := time.Since(sendStart).Nanoseconds() + channelSendTotalNs.Add(sendNs) + for { + old := channelSendMaxNs.Load() + if sendNs <= old || channelSendMaxNs.CompareAndSwap(old, sendNs) { + break + } + } return } @@ -409,7 +469,16 @@ func (e *Engine) Execute(ctx context.Context, q *sdp.Query, adapter Adapter, res // Send the item back to the caller numItems.Add(1) + sendStart := time.Now() responses <- sdp.NewQueryResponseFromItem(item) + sendNs := time.Since(sendStart).Nanoseconds() + channelSendTotalNs.Add(sendNs) + for { + old := channelSendMaxNs.Load() + if sendNs <= old || channelSendMaxNs.CompareAndSwap(old, sendNs) { + break + } + } } var errHandler ErrHandler = func(err error) { if err == nil { @@ -423,7 +492,16 @@ func (e *Engine) Execute(ctx context.Context, q *sdp.Query, adapter Adapter, res // Send the error back to the caller numErrs.Add(1) + sendStart := time.Now() responses <- queryResponseFromError(err, q, adapter, e.EngineConfig.SourceName) + sendNs := time.Since(sendStart).Nanoseconds() + channelSendTotalNs.Add(sendNs) + for { + old := channelSendMaxNs.Load() + if sendNs <= old || channelSendMaxNs.CompareAndSwap(old, sendNs) { + break + } + } } stream := NewQueryResultStream(itemHandler, errHandler) @@ -506,6 +584,8 @@ func (e *Engine) Execute(ctx context.Context, q *sdp.Query, adapter Adapter, res span.SetAttributes( attribute.Int("ovm.adapter.numItems", int(numItems.Load())), attribute.Int("ovm.adapter.numErrors", int(numErrs.Load())), + attribute.Float64("ovm.discovery.channelSendMaxMs", float64(channelSendMaxNs.Load())/1e6), + attribute.Float64("ovm.discovery.channelSendTotalMs", float64(channelSendTotalNs.Load())/1e6), ) } diff --git a/go/discovery/querytracker.go b/go/discovery/querytracker.go index 4cb30c97..e1755f94 100644 --- a/go/discovery/querytracker.go +++ b/go/discovery/querytracker.go @@ -3,6 +3,7 @@ package discovery import ( "context" "errors" + "time" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/go/tracing" @@ -63,9 +64,20 @@ func (qt *QueryTracker) Execute(ctx context.Context) ([]*sdp.Item, []*sdp.Edge, }(errChan) // Process the responses as they come in + var natsPublishMaxNs int64 + var natsPublishTotalNs int64 + var natsPublishCount int + for response := range responses { if qt.Query.Subject() != "" && qt.Engine.natsConnection != nil { + publishStart := time.Now() err := qt.Engine.natsConnection.Publish(ctx, qt.Query.Subject(), response) + publishNs := time.Since(publishStart).Nanoseconds() + natsPublishTotalNs += publishNs + natsPublishCount++ + if publishNs > natsPublishMaxNs { + natsPublishMaxNs = publishNs + } if err != nil { span.RecordError(err) log.WithError(err).Error("Response publishing error") @@ -82,6 +94,12 @@ func (qt *QueryTracker) Execute(ctx context.Context) ([]*sdp.Item, []*sdp.Edge, } } + span.SetAttributes( + attribute.Float64("ovm.nats.publishMaxMs", float64(natsPublishMaxNs)/1e6), + attribute.Float64("ovm.nats.publishTotalMs", float64(natsPublishTotalNs)/1e6), + attribute.Int("ovm.nats.publishCount", natsPublishCount), + ) + // Get the result of the execution err := <-errChan if err != nil { diff --git a/k8s-source/adapters/endpoints.go b/k8s-source/adapters/endpoints.go index ef90ec11..0fb050eb 100644 --- a/k8s-source/adapters/endpoints.go +++ b/k8s-source/adapters/endpoints.go @@ -8,7 +8,7 @@ import ( "k8s.io/client-go/kubernetes" ) -func EndpointsExtractor(resource *v1.Endpoints, scope string) ([]*sdp.LinkedItemQuery, error) { //nolint:staticcheck +func EndpointsExtractor(resource *v1.Endpoints, scope string) ([]*sdp.LinkedItemQuery, error) { queries := make([]*sdp.LinkedItemQuery, 0) sd, err := ParseScope(scope, true) @@ -62,15 +62,15 @@ func EndpointsExtractor(resource *v1.Endpoints, scope string) ([]*sdp.LinkedItem } func newEndpointsAdapter(cs *kubernetes.Clientset, cluster string, namespaces []string, cache sdpcache.Cache) discovery.ListableAdapter { - return &KubeTypeAdapter[*v1.Endpoints, *v1.EndpointsList]{ //nolint:staticcheck + return &KubeTypeAdapter[*v1.Endpoints, *v1.EndpointsList]{ ClusterName: cluster, Namespaces: namespaces, TypeName: "Endpoints", - NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.Endpoints, *v1.EndpointsList] { //nolint:staticcheck + NamespacedInterfaceBuilder: func(namespace string) ItemInterface[*v1.Endpoints, *v1.EndpointsList] { return cs.CoreV1().Endpoints(namespace) }, - ListExtractor: func(list *v1.EndpointsList) ([]*v1.Endpoints, error) { //nolint:staticcheck - extracted := make([]*v1.Endpoints, len(list.Items)) //nolint:staticcheck + ListExtractor: func(list *v1.EndpointsList) ([]*v1.Endpoints, error) { + extracted := make([]*v1.Endpoints, len(list.Items)) for i := range list.Items { extracted[i] = &list.Items[i] From 37484404a38e2a2e9e28a2ddbac46e9783d7ae19 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:47:04 +0000 Subject: [PATCH 47/74] Add SQL Server Private Endpoint Connection Adapter and Tests (#4090) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image --- > [!NOTE] > **Medium Risk** > Mostly additive Azure discovery code, but it introduces new Azure SDK client usage and query/linking logic that could affect discovery completeness and required IAM permissions in production subscriptions. > > **Overview** > Adds support for discovering Azure SQL Server *private endpoint connections* via a new searchable wrapper (`NewSQLServerPrivateEndpointConnection`) backed by a dedicated `SQLServerPrivateEndpointConnectionsClient`. > > The new adapter maps provisioning state to item health, sets a composite unique key (`serverName` + connection name), and emits links to the parent `SQLServer` and (when present) the referenced `NetworkPrivateEndpoint` (including cross-resource-group scope extraction). It’s wired into `manual/adapters.go` (real and placeholder adapters), includes generated GoMock client + a full unit test suite, and updates Azure resource-ID path parsing to recognize `azure-sql-server-private-endpoint-connection`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d6daf7588e1000b1ee7c6ce9cf90bb6d3f836d47. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 7a6a980ea78af4f9f0f1cf8424465ef4f48eb00e --- ...rver-private-endpoint-connection-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + .../sql-server-private-endpoint-connection.go | 236 +++++++++++++ ...server-private-endpoint-connection_test.go | 322 ++++++++++++++++++ ...rver_private_endpoint_connection_client.go | 72 ++++ sources/azure/shared/utils.go | 1 + 6 files changed, 676 insertions(+) create mode 100644 sources/azure/clients/sql-server-private-endpoint-connection-client.go create mode 100644 sources/azure/manual/sql-server-private-endpoint-connection.go create mode 100644 sources/azure/manual/sql-server-private-endpoint-connection_test.go create mode 100644 sources/azure/shared/mocks/mock_sql_server_private_endpoint_connection_client.go diff --git a/sources/azure/clients/sql-server-private-endpoint-connection-client.go b/sources/azure/clients/sql-server-private-endpoint-connection-client.go new file mode 100644 index 00000000..f262a7fc --- /dev/null +++ b/sources/azure/clients/sql-server-private-endpoint-connection-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" +) + +//go:generate mockgen -destination=../shared/mocks/mock_sql_server_private_endpoint_connection_client.go -package=mocks -source=sql-server-private-endpoint-connection-client.go + +// SQLServerPrivateEndpointConnectionsPager is a type alias for the generic Pager interface with SQL server private endpoint connection list response type. +type SQLServerPrivateEndpointConnectionsPager = Pager[armsql.PrivateEndpointConnectionsClientListByServerResponse] + +// SQLServerPrivateEndpointConnectionsClient is an interface for interacting with Azure SQL server private endpoint connections. +type SQLServerPrivateEndpointConnectionsClient interface { + Get(ctx context.Context, resourceGroupName string, serverName string, privateEndpointConnectionName string) (armsql.PrivateEndpointConnectionsClientGetResponse, error) + ListByServer(ctx context.Context, resourceGroupName string, serverName string) SQLServerPrivateEndpointConnectionsPager +} + +type sqlServerPrivateEndpointConnectionsClient struct { + client *armsql.PrivateEndpointConnectionsClient +} + +func (c *sqlServerPrivateEndpointConnectionsClient) Get(ctx context.Context, resourceGroupName string, serverName string, privateEndpointConnectionName string) (armsql.PrivateEndpointConnectionsClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, serverName, privateEndpointConnectionName, nil) +} + +func (c *sqlServerPrivateEndpointConnectionsClient) ListByServer(ctx context.Context, resourceGroupName string, serverName string) SQLServerPrivateEndpointConnectionsPager { + return c.client.NewListByServerPager(resourceGroupName, serverName, nil) +} + +// NewSQLServerPrivateEndpointConnectionsClient creates a new SQLServerPrivateEndpointConnectionsClient from the Azure SDK client. +func NewSQLServerPrivateEndpointConnectionsClient(client *armsql.PrivateEndpointConnectionsClient) SQLServerPrivateEndpointConnectionsClient { + return &sqlServerPrivateEndpointConnectionsClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 5794734d..e95d6bf8 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -255,6 +255,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create sql elastic pools client: %w", err) } + sqlPrivateEndpointConnectionsClient, err := armsql.NewPrivateEndpointConnectionsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create sql private endpoint connections client: %w", err) + } + postgresqlFlexibleServersClient, err := armpostgresqlflexibleservers.NewServersClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create postgresql flexible servers client: %w", err) @@ -435,6 +440,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewSqlServerVirtualNetworkRuleClient(sqlVirtualNetworkRulesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewSQLServerPrivateEndpointConnection( + clients.NewSQLServerPrivateEndpointConnectionsClient(sqlPrivateEndpointConnectionsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewDocumentDBDatabaseAccounts( clients.NewDocumentDBDatabaseAccountsClient(documentDBDatabaseAccountsClient), resourceGroupScopes, @@ -641,6 +650,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewSqlDatabase(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewSqlServerFirewallRule(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewSqlServerVirtualNetworkRule(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewSQLServerPrivateEndpointConnection(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDocumentDBDatabaseAccounts(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDocumentDBPrivateEndpointConnection(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewKeyVaultVault(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/sql-server-private-endpoint-connection.go b/sources/azure/manual/sql-server-private-endpoint-connection.go new file mode 100644 index 00000000..9665cb3c --- /dev/null +++ b/sources/azure/manual/sql-server-private-endpoint-connection.go @@ -0,0 +1,236 @@ +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" + "github.com/overmindtech/cli/sources/shared" +) + +var SQLServerPrivateEndpointConnectionLookupByName = shared.NewItemTypeLookup("name", azureshared.SQLServerPrivateEndpointConnection) + +type sqlServerPrivateEndpointConnectionWrapper struct { + client clients.SQLServerPrivateEndpointConnectionsClient + + *azureshared.MultiResourceGroupBase +} + +// NewSQLServerPrivateEndpointConnection returns a SearchableWrapper for Azure SQL server private endpoint connections. +func NewSQLServerPrivateEndpointConnection(client clients.SQLServerPrivateEndpointConnectionsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &sqlServerPrivateEndpointConnectionWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_DATABASE, + azureshared.SQLServerPrivateEndpointConnection, + ), + } +} + +func (s sqlServerPrivateEndpointConnectionWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: serverName and privateEndpointConnectionName", + Scope: scope, + ItemType: s.Type(), + } + } + serverName := queryParts[0] + connectionName := queryParts[1] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + resp, err := s.client.Get(ctx, rgScope.ResourceGroup, serverName, connectionName) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + item, sdpErr := s.azurePrivateEndpointConnectionToSDPItem(&resp.PrivateEndpointConnection, serverName, connectionName, scope) + if sdpErr != nil { + return nil, sdpErr + } + return item, nil +} + +func (s sqlServerPrivateEndpointConnectionWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + SQLServerLookupByName, + SQLServerPrivateEndpointConnectionLookupByName, + } +} + +func (s sqlServerPrivateEndpointConnectionWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: serverName", + Scope: scope, + ItemType: s.Type(), + } + } + serverName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + pager := s.client.ListByServer(ctx, rgScope.ResourceGroup, serverName) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + for _, conn := range page.Value { + if conn == nil || conn.Name == nil { + continue + } + + item, sdpErr := s.azurePrivateEndpointConnectionToSDPItem(conn, serverName, *conn.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (s sqlServerPrivateEndpointConnectionWrapper) 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 _, conn := range page.Value { + if conn == nil || conn.Name == nil { + continue + } + item, sdpErr := s.azurePrivateEndpointConnectionToSDPItem(conn, serverName, *conn.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (s sqlServerPrivateEndpointConnectionWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + SQLServerLookupByName, + }, + } +} + +func (s sqlServerPrivateEndpointConnectionWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.SQLServer: true, + azureshared.NetworkPrivateEndpoint: true, + } +} + +func (s sqlServerPrivateEndpointConnectionWrapper) azurePrivateEndpointConnectionToSDPItem(conn *armsql.PrivateEndpointConnection, serverName, connectionName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(conn) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(serverName, connectionName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.SQLServerPrivateEndpointConnection.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + } + + // Health from provisioning state (armsql uses PrivateEndpointProvisioningState enum) + if conn.Properties != nil && conn.Properties.ProvisioningState != nil { + state := strings.ToLower(string(*conn.Properties.ProvisioningState)) + switch state { + case "ready": + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case "approving", "dropping": + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case "failed", "rejecting": + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + // Link to parent SQL Server + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.SQLServer.String(), + Method: sdp.QueryMethod_GET, + Query: serverName, + Scope: scope, + }, + }) + + // Link to Network Private Endpoint when present (may be in different resource group) + if conn.Properties != nil && conn.Properties.PrivateEndpoint != nil && conn.Properties.PrivateEndpoint.ID != nil { + peID := *conn.Properties.PrivateEndpoint.ID + peName := azureshared.ExtractResourceName(peID) + if peName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(peID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPrivateEndpoint.String(), + Method: sdp.QueryMethod_GET, + Query: peName, + Scope: linkedScope, + }, + }) + } + } + + return sdpItem, nil +} + +func (s sqlServerPrivateEndpointConnectionWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Sql/servers/privateEndpointConnections/read", + } +} + +func (s sqlServerPrivateEndpointConnectionWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/sql-server-private-endpoint-connection_test.go b/sources/azure/manual/sql-server-private-endpoint-connection_test.go new file mode 100644 index 00000000..8108db7c --- /dev/null +++ b/sources/azure/manual/sql-server-private-endpoint-connection_test.go @@ -0,0 +1,322 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" + "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" +) + +type mockSQLServerPrivateEndpointConnectionsPager struct { + pages []armsql.PrivateEndpointConnectionsClientListByServerResponse + index int +} + +func (m *mockSQLServerPrivateEndpointConnectionsPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockSQLServerPrivateEndpointConnectionsPager) NextPage(ctx context.Context) (armsql.PrivateEndpointConnectionsClientListByServerResponse, error) { + if m.index >= len(m.pages) { + return armsql.PrivateEndpointConnectionsClientListByServerResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type testSQLServerPrivateEndpointConnectionsClient struct { + *mocks.MockSQLServerPrivateEndpointConnectionsClient + pager clients.SQLServerPrivateEndpointConnectionsPager +} + +func (t *testSQLServerPrivateEndpointConnectionsClient) ListByServer(ctx context.Context, resourceGroupName, serverName string) clients.SQLServerPrivateEndpointConnectionsPager { + return t.pager +} + +func TestSQLServerPrivateEndpointConnection(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + serverName := "test-sql-server" + connectionName := "test-pec" + + t.Run("Get", func(t *testing.T) { + conn := createAzureSQLServerPrivateEndpointConnection(connectionName, "") + + mockClient := mocks.NewMockSQLServerPrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, connectionName).Return( + armsql.PrivateEndpointConnectionsClientGetResponse{ + PrivateEndpointConnection: *conn, + }, nil) + + testClient := &testSQLServerPrivateEndpointConnectionsClient{MockSQLServerPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewSQLServerPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, connectionName) + 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.SQLServerPrivateEndpointConnection.String() { + t.Errorf("Expected type %s, got %s", azureshared.SQLServerPrivateEndpointConnection, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != shared.CompositeLookupKey(serverName, connectionName) { + t.Errorf("Expected unique attribute value %s, got %s", shared.CompositeLookupKey(serverName, connectionName), sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + linkedQueries := sdpItem.GetLinkedItemQueries() + if len(linkedQueries) < 1 { + t.Fatalf("Expected at least 1 linked query, got: %d", len(linkedQueries)) + } + + foundSQLServer := false + for _, lq := range linkedQueries { + if lq.GetQuery().GetType() == azureshared.SQLServer.String() { + foundSQLServer = true + if lq.GetQuery().GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected SQLServer link method GET, got %v", lq.GetQuery().GetMethod()) + } + if lq.GetQuery().GetQuery() != serverName { + t.Errorf("Expected SQLServer query %s, got %s", serverName, lq.GetQuery().GetQuery()) + } + } + } + if !foundSQLServer { + t.Error("Expected linked query to SQLServer") + } + }) + }) + + t.Run("Get_WithPrivateEndpointLink", func(t *testing.T) { + peID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-pe" + conn := createAzureSQLServerPrivateEndpointConnection(connectionName, peID) + + mockClient := mocks.NewMockSQLServerPrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, connectionName).Return( + armsql.PrivateEndpointConnectionsClientGetResponse{ + PrivateEndpointConnection: *conn, + }, nil) + + testClient := &testSQLServerPrivateEndpointConnectionsClient{MockSQLServerPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewSQLServerPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, connectionName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + foundPrivateEndpoint := false + for _, lq := range sdpItem.GetLinkedItemQueries() { + if lq.GetQuery().GetType() == azureshared.NetworkPrivateEndpoint.String() { + foundPrivateEndpoint = true + if lq.GetQuery().GetQuery() != "test-pe" { + t.Errorf("Expected NetworkPrivateEndpoint query 'test-pe', got %s", lq.GetQuery().GetQuery()) + } + break + } + } + if !foundPrivateEndpoint { + t.Error("Expected linked query to NetworkPrivateEndpoint when PrivateEndpoint ID is set") + } + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSQLServerPrivateEndpointConnectionsClient(ctrl) + testClient := &testSQLServerPrivateEndpointConnectionsClient{MockSQLServerPrivateEndpointConnectionsClient: mockClient} + + wrapper := manual.NewSQLServerPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], serverName, true) + if qErr == nil { + t.Error("Expected error when providing insufficient query parts, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + conn1 := createAzureSQLServerPrivateEndpointConnection("pec-1", "") + conn2 := createAzureSQLServerPrivateEndpointConnection("pec-2", "") + + mockClient := mocks.NewMockSQLServerPrivateEndpointConnectionsClient(ctrl) + mockPager := &mockSQLServerPrivateEndpointConnectionsPager{ + pages: []armsql.PrivateEndpointConnectionsClientListByServerResponse{ + { + PrivateEndpointConnectionListResult: armsql.PrivateEndpointConnectionListResult{ + Value: []*armsql.PrivateEndpointConnection{conn1, conn2}, + }, + }, + }, + } + + testClient := &testSQLServerPrivateEndpointConnectionsClient{ + MockSQLServerPrivateEndpointConnectionsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewSQLServerPrivateEndpointConnection(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, wrapper.Scopes()[0], serverName, 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.Fatalf("Expected no validation error, got: %v", err) + } + if item.GetType() != azureshared.SQLServerPrivateEndpointConnection.String() { + t.Errorf("Expected type %s, got %s", azureshared.SQLServerPrivateEndpointConnection, item.GetType()) + } + } + }) + + t.Run("Search_NilNameSkipped", func(t *testing.T) { + validConn := createAzureSQLServerPrivateEndpointConnection("valid-pec", "") + + mockClient := mocks.NewMockSQLServerPrivateEndpointConnectionsClient(ctrl) + mockPager := &mockSQLServerPrivateEndpointConnectionsPager{ + pages: []armsql.PrivateEndpointConnectionsClientListByServerResponse{ + { + PrivateEndpointConnectionListResult: armsql.PrivateEndpointConnectionListResult{ + Value: []*armsql.PrivateEndpointConnection{ + {Name: nil}, + validConn, + }, + }, + }, + }, + } + + testClient := &testSQLServerPrivateEndpointConnectionsClient{ + MockSQLServerPrivateEndpointConnectionsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewSQLServerPrivateEndpointConnection(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, wrapper.Scopes()[0], serverName, 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)) + } + if sdpItems[0].UniqueAttributeValue() != shared.CompositeLookupKey(serverName, "valid-pec") { + t.Errorf("Expected unique value %s, got %s", shared.CompositeLookupKey(serverName, "valid-pec"), sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockSQLServerPrivateEndpointConnectionsClient(ctrl) + testClient := &testSQLServerPrivateEndpointConnectionsClient{MockSQLServerPrivateEndpointConnectionsClient: mockClient} + + wrapper := manual.NewSQLServerPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("private endpoint connection not found") + + mockClient := mocks.NewMockSQLServerPrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, "nonexistent-pec").Return( + armsql.PrivateEndpointConnectionsClientGetResponse{}, expectedErr) + + testClient := &testSQLServerPrivateEndpointConnectionsClient{MockSQLServerPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewSQLServerPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, "nonexistent-pec") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent private endpoint connection, but got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + wrapper := manual.NewSQLServerPrivateEndpointConnection(nil, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + links := wrapper.PotentialLinks() + if !links[azureshared.SQLServer] { + t.Error("Expected SQLServer in PotentialLinks") + } + if !links[azureshared.NetworkPrivateEndpoint] { + t.Error("Expected NetworkPrivateEndpoint in PotentialLinks") + } + }) +} + +func createAzureSQLServerPrivateEndpointConnection(connectionName, privateEndpointID string) *armsql.PrivateEndpointConnection { + ready := armsql.PrivateEndpointProvisioningStateReady + approved := armsql.PrivateLinkServiceConnectionStateStatusApproved + conn := &armsql.PrivateEndpointConnection{ + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.Sql/servers/test-sql-server/privateEndpointConnections/" + connectionName), + Name: new(connectionName), + Type: new("Microsoft.Sql/servers/privateEndpointConnections"), + Properties: &armsql.PrivateEndpointConnectionProperties{ + ProvisioningState: &ready, + PrivateLinkServiceConnectionState: &armsql.PrivateLinkServiceConnectionStateProperty{ + Status: &approved, + }, + }, + } + if privateEndpointID != "" { + conn.Properties.PrivateEndpoint = &armsql.PrivateEndpointProperty{ + ID: new(privateEndpointID), + } + } + return conn +} diff --git a/sources/azure/shared/mocks/mock_sql_server_private_endpoint_connection_client.go b/sources/azure/shared/mocks/mock_sql_server_private_endpoint_connection_client.go new file mode 100644 index 00000000..70c814fb --- /dev/null +++ b/sources/azure/shared/mocks/mock_sql_server_private_endpoint_connection_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: sql-server-private-endpoint-connection-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_sql_server_private_endpoint_connection_client.go -package=mocks -source=sql-server-private-endpoint-connection-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armsql "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockSQLServerPrivateEndpointConnectionsClient is a mock of SQLServerPrivateEndpointConnectionsClient interface. +type MockSQLServerPrivateEndpointConnectionsClient struct { + ctrl *gomock.Controller + recorder *MockSQLServerPrivateEndpointConnectionsClientMockRecorder + isgomock struct{} +} + +// MockSQLServerPrivateEndpointConnectionsClientMockRecorder is the mock recorder for MockSQLServerPrivateEndpointConnectionsClient. +type MockSQLServerPrivateEndpointConnectionsClientMockRecorder struct { + mock *MockSQLServerPrivateEndpointConnectionsClient +} + +// NewMockSQLServerPrivateEndpointConnectionsClient creates a new mock instance. +func NewMockSQLServerPrivateEndpointConnectionsClient(ctrl *gomock.Controller) *MockSQLServerPrivateEndpointConnectionsClient { + mock := &MockSQLServerPrivateEndpointConnectionsClient{ctrl: ctrl} + mock.recorder = &MockSQLServerPrivateEndpointConnectionsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSQLServerPrivateEndpointConnectionsClient) EXPECT() *MockSQLServerPrivateEndpointConnectionsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockSQLServerPrivateEndpointConnectionsClient) Get(ctx context.Context, resourceGroupName, serverName, privateEndpointConnectionName string) (armsql.PrivateEndpointConnectionsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, serverName, privateEndpointConnectionName) + ret0, _ := ret[0].(armsql.PrivateEndpointConnectionsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockSQLServerPrivateEndpointConnectionsClientMockRecorder) Get(ctx, resourceGroupName, serverName, privateEndpointConnectionName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockSQLServerPrivateEndpointConnectionsClient)(nil).Get), ctx, resourceGroupName, serverName, privateEndpointConnectionName) +} + +// ListByServer mocks base method. +func (m *MockSQLServerPrivateEndpointConnectionsClient) ListByServer(ctx context.Context, resourceGroupName, serverName string) clients.SQLServerPrivateEndpointConnectionsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByServer", ctx, resourceGroupName, serverName) + ret0, _ := ret[0].(clients.SQLServerPrivateEndpointConnectionsPager) + return ret0 +} + +// ListByServer indicates an expected call of ListByServer. +func (mr *MockSQLServerPrivateEndpointConnectionsClientMockRecorder) ListByServer(ctx, resourceGroupName, serverName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByServer", reflect.TypeOf((*MockSQLServerPrivateEndpointConnectionsClient)(nil).ListByServer), ctx, resourceGroupName, serverName) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index bcc1dd78..7723fea3 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -29,6 +29,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-sql-elastic-pool": {"servers", "elasticPools"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/elasticPools/{elasticPoolName}", "azure-sql-server-firewall-rule": {"servers", "firewallRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/firewallRules/{ruleName}", "azure-sql-server-virtual-network-rule": {"servers", "virtualNetworkRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/virtualNetworkRules/{ruleName}", + "azure-sql-server-private-endpoint-connection": {"servers", "privateEndpointConnections"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/privateEndpointConnections/{connectionName}", "azure-dbforpostgresql-database": {"flexibleServers", "databases"}, // "/subscriptions/.../Microsoft.DBforPostgreSQL/flexibleServers/{server}/databases/{db}", "azure-dbforpostgresql-flexible-server-firewall-rule": {"flexibleServers", "firewallRules"}, // "/subscriptions/.../Microsoft.DBforPostgreSQL/flexibleServers/{server}/firewallRules/{rule}", "azure-keyvault-secret": {"vaults", "secrets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{vaultName}/secrets/{secretName}", From 3e179ac027eca99f4bdac9ef2dc2680c87c3d4a2 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:48:29 +0000 Subject: [PATCH 48/74] Eng 2882 create dbforpostgresqlflexibleserver private endpoint connection adapter (#4092) image > [!NOTE] > **Medium Risk** > Adds new Azure discovery adapters and initializes additional Azure SDK clients, which may affect API permissions, paging behavior, and cross-scope linking for newly ingested resources. Other changes are low impact lint/comment cleanups. > > **Overview** > Adds discovery support for Azure *private endpoint connections* on **SQL Servers** and **DB for PostgreSQL Flexible Servers**, including new client wrappers, adapters/wrappers with `Get`/`Search`/streaming implementations, linked-item queries to the parent server and referenced `NetworkPrivateEndpoint` (with scope extraction), and IAM permission declarations. > > Wires the new adapters into the Azure manual adapter set (including placeholder registration), generates new gomock clients, and extends Azure resource-ID path key extraction to recognize the two new item types. > > Separately removes a few `//nolint` suppressions in the k8s `Endpoints` adapter and the gateway `ListenAndServe` startup code without functional changes. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit abe886cf7b52c91cbc90ff3c983d8fb35cd53fe4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: e5d29bb5246bbed785c69103aaf36287ac186377 --- ...rver-private-endpoint-connection-client.go | 35 +++ sources/azure/manual/adapters.go | 10 + ...ible-server-private-endpoint-connection.go | 236 +++++++++++++++ ...server-private-endpoint-connection_test.go | 278 ++++++++++++++++++ ...rver_private_endpoint_connection_client.go | 72 +++++ sources/azure/shared/utils.go | 1 + 6 files changed, 632 insertions(+) create mode 100644 sources/azure/clients/dbforpostgresql-flexible-server-private-endpoint-connection-client.go create mode 100644 sources/azure/manual/dbforpostgresql-flexible-server-private-endpoint-connection.go create mode 100644 sources/azure/manual/dbforpostgresql-flexible-server-private-endpoint-connection_test.go create mode 100644 sources/azure/shared/mocks/mock_dbforpostgresql_flexible_server_private_endpoint_connection_client.go diff --git a/sources/azure/clients/dbforpostgresql-flexible-server-private-endpoint-connection-client.go b/sources/azure/clients/dbforpostgresql-flexible-server-private-endpoint-connection-client.go new file mode 100644 index 00000000..cfccd927 --- /dev/null +++ b/sources/azure/clients/dbforpostgresql-flexible-server-private-endpoint-connection-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" +) + +//go:generate mockgen -destination=../shared/mocks/mock_dbforpostgresql_flexible_server_private_endpoint_connection_client.go -package=mocks -source=dbforpostgresql-flexible-server-private-endpoint-connection-client.go + +// DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsPager is a type alias for the generic Pager interface with PostgreSQL flexible server private endpoint connection list response type. +type DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsPager = Pager[armpostgresqlflexibleservers.PrivateEndpointConnectionsClientListByServerResponse] + +// DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient is an interface for interacting with Azure DB for PostgreSQL flexible server private endpoint connections. +type DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient interface { + Get(ctx context.Context, resourceGroupName string, serverName string, privateEndpointConnectionName string) (armpostgresqlflexibleservers.PrivateEndpointConnectionsClientGetResponse, error) + ListByServer(ctx context.Context, resourceGroupName string, serverName string) DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsPager +} + +type dbforpostgresqlFlexibleServerPrivateEndpointConnectionsClient struct { + client *armpostgresqlflexibleservers.PrivateEndpointConnectionsClient +} + +func (c *dbforpostgresqlFlexibleServerPrivateEndpointConnectionsClient) Get(ctx context.Context, resourceGroupName string, serverName string, privateEndpointConnectionName string) (armpostgresqlflexibleservers.PrivateEndpointConnectionsClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, serverName, privateEndpointConnectionName, nil) +} + +func (c *dbforpostgresqlFlexibleServerPrivateEndpointConnectionsClient) ListByServer(ctx context.Context, resourceGroupName string, serverName string) DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsPager { + return c.client.NewListByServerPager(resourceGroupName, serverName, nil) +} + +// NewDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient creates a new DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient from the Azure SDK client. +func NewDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient(client *armpostgresqlflexibleservers.PrivateEndpointConnectionsClient) DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient { + return &dbforpostgresqlFlexibleServerPrivateEndpointConnectionsClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index e95d6bf8..6925095f 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -270,6 +270,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create postgresql firewall rules client: %w", err) } + postgresqlPrivateEndpointConnectionsClient, err := armpostgresqlflexibleservers.NewPrivateEndpointConnectionsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create postgresql flexible server private endpoint connections client: %w", err) + } + secretsClient, err := armkeyvault.NewSecretsClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create secrets client: %w", err) @@ -536,6 +541,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewPostgreSQLFlexibleServerFirewallRuleClient(postgresqlFirewallRulesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServerPrivateEndpointConnection( + clients.NewDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient(postgresqlPrivateEndpointConnectionsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewKeyVaultSecret( clients.NewSecretsClient(secretsClient), resourceGroupScopes, @@ -672,6 +681,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewSqlServer(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServer(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServerFirewallRule(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServerPrivateEndpointConnection(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewKeyVaultSecret(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewKeyVaultKey(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewManagedIdentityUserAssignedIdentity(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/dbforpostgresql-flexible-server-private-endpoint-connection.go b/sources/azure/manual/dbforpostgresql-flexible-server-private-endpoint-connection.go new file mode 100644 index 00000000..00bdb154 --- /dev/null +++ b/sources/azure/manual/dbforpostgresql-flexible-server-private-endpoint-connection.go @@ -0,0 +1,236 @@ +package manual + +import ( + "context" + "errors" + "strings" + + "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" + "github.com/overmindtech/cli/sources/shared" +) + +var DBforPostgreSQLFlexibleServerPrivateEndpointConnectionLookupByName = shared.NewItemTypeLookup("name", azureshared.DBforPostgreSQLFlexibleServerPrivateEndpointConnection) + +type dbforpostgresqlFlexibleServerPrivateEndpointConnectionWrapper struct { + client clients.DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient + + *azureshared.MultiResourceGroupBase +} + +// NewDBforPostgreSQLFlexibleServerPrivateEndpointConnection returns a SearchableWrapper for Azure DB for PostgreSQL flexible server private endpoint connections. +func NewDBforPostgreSQLFlexibleServerPrivateEndpointConnection(client clients.DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &dbforpostgresqlFlexibleServerPrivateEndpointConnectionWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_DATABASE, + azureshared.DBforPostgreSQLFlexibleServerPrivateEndpointConnection, + ), + } +} + +func (s dbforpostgresqlFlexibleServerPrivateEndpointConnectionWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: serverName and privateEndpointConnectionName", + Scope: scope, + ItemType: s.Type(), + } + } + serverName := queryParts[0] + connectionName := queryParts[1] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + resp, err := s.client.Get(ctx, rgScope.ResourceGroup, serverName, connectionName) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + item, sdpErr := s.azurePrivateEndpointConnectionToSDPItem(&resp.PrivateEndpointConnection, serverName, connectionName, scope) + if sdpErr != nil { + return nil, sdpErr + } + return item, nil +} + +func (s dbforpostgresqlFlexibleServerPrivateEndpointConnectionWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + DBforPostgreSQLFlexibleServerLookupByName, + DBforPostgreSQLFlexibleServerPrivateEndpointConnectionLookupByName, + } +} + +func (s dbforpostgresqlFlexibleServerPrivateEndpointConnectionWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: serverName", + Scope: scope, + ItemType: s.Type(), + } + } + serverName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + pager := s.client.ListByServer(ctx, rgScope.ResourceGroup, serverName) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + for _, conn := range page.Value { + if conn == nil || conn.Name == nil { + continue + } + + item, sdpErr := s.azurePrivateEndpointConnectionToSDPItem(conn, serverName, *conn.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (s dbforpostgresqlFlexibleServerPrivateEndpointConnectionWrapper) 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 _, conn := range page.Value { + if conn == nil || conn.Name == nil { + continue + } + item, sdpErr := s.azurePrivateEndpointConnectionToSDPItem(conn, serverName, *conn.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (s dbforpostgresqlFlexibleServerPrivateEndpointConnectionWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + DBforPostgreSQLFlexibleServerLookupByName, + }, + } +} + +func (s dbforpostgresqlFlexibleServerPrivateEndpointConnectionWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.DBforPostgreSQLFlexibleServer: true, + azureshared.NetworkPrivateEndpoint: true, + } +} + +func (s dbforpostgresqlFlexibleServerPrivateEndpointConnectionWrapper) azurePrivateEndpointConnectionToSDPItem(conn *armpostgresqlflexibleservers.PrivateEndpointConnection, serverName, connectionName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(conn) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(serverName, connectionName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.DBforPostgreSQLFlexibleServerPrivateEndpointConnection.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + } + + // Health from provisioning state + if conn.Properties != nil && conn.Properties.ProvisioningState != nil { + state := strings.ToLower(string(*conn.Properties.ProvisioningState)) + switch state { + case "succeeded": + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case "creating", "updating", "deleting": + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case "failed": + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + // Link to parent DB for PostgreSQL Flexible Server + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.DBforPostgreSQLFlexibleServer.String(), + Method: sdp.QueryMethod_GET, + Query: serverName, + Scope: scope, + }, + }) + + // Link to Network Private Endpoint when present (may be in different resource group) + if conn.Properties != nil && conn.Properties.PrivateEndpoint != nil && conn.Properties.PrivateEndpoint.ID != nil { + peID := *conn.Properties.PrivateEndpoint.ID + peName := azureshared.ExtractResourceName(peID) + if peName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(peID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPrivateEndpoint.String(), + Method: sdp.QueryMethod_GET, + Query: peName, + Scope: linkedScope, + }, + }) + } + } + + return sdpItem, nil +} + +func (s dbforpostgresqlFlexibleServerPrivateEndpointConnectionWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.DBforPostgreSQL/flexibleServers/privateEndpointConnections/read", + } +} + +func (s dbforpostgresqlFlexibleServerPrivateEndpointConnectionWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/dbforpostgresql-flexible-server-private-endpoint-connection_test.go b/sources/azure/manual/dbforpostgresql-flexible-server-private-endpoint-connection_test.go new file mode 100644 index 00000000..22334460 --- /dev/null +++ b/sources/azure/manual/dbforpostgresql-flexible-server-private-endpoint-connection_test.go @@ -0,0 +1,278 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" + "go.uber.org/mock/gomock" + + "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" +) + +type mockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsPager struct { + pages []armpostgresqlflexibleservers.PrivateEndpointConnectionsClientListByServerResponse + index int +} + +func (m *mockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsPager) NextPage(ctx context.Context) (armpostgresqlflexibleservers.PrivateEndpointConnectionsClientListByServerResponse, error) { + if m.index >= len(m.pages) { + return armpostgresqlflexibleservers.PrivateEndpointConnectionsClientListByServerResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type testDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient struct { + *mocks.MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient + pager clients.DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsPager +} + +func (t *testDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient) ListByServer(ctx context.Context, resourceGroupName, serverName string) clients.DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsPager { + return t.pager +} + +func TestDBforPostgreSQLFlexibleServerPrivateEndpointConnection(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + serverName := "test-pg-server" + connectionName := "test-pec" + + t.Run("Get", func(t *testing.T) { + conn := createAzureDBforPostgreSQLFlexibleServerPrivateEndpointConnection(connectionName, "") + + mockClient := mocks.NewMockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, connectionName).Return( + armpostgresqlflexibleservers.PrivateEndpointConnectionsClientGetResponse{ + PrivateEndpointConnection: *conn, + }, nil) + + testClient := &testDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient{MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewDBforPostgreSQLFlexibleServerPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, connectionName) + 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.DBforPostgreSQLFlexibleServerPrivateEndpointConnection.String() { + t.Errorf("Expected type %s, got %s", azureshared.DBforPostgreSQLFlexibleServerPrivateEndpointConnection, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != shared.CompositeLookupKey(serverName, connectionName) { + t.Errorf("Expected unique attribute value %s, got %s", shared.CompositeLookupKey(serverName, connectionName), sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + linkedQueries := sdpItem.GetLinkedItemQueries() + if len(linkedQueries) < 1 { + t.Fatalf("Expected at least 1 linked query, got: %d", len(linkedQueries)) + } + + foundFlexibleServer := false + for _, lq := range linkedQueries { + if lq.GetQuery().GetType() == azureshared.DBforPostgreSQLFlexibleServer.String() { + foundFlexibleServer = true + if lq.GetQuery().GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected DBforPostgreSQLFlexibleServer link method GET, got %v", lq.GetQuery().GetMethod()) + } + if lq.GetQuery().GetQuery() != serverName { + t.Errorf("Expected DBforPostgreSQLFlexibleServer query %s, got %s", serverName, lq.GetQuery().GetQuery()) + } + } + } + if !foundFlexibleServer { + t.Error("Expected linked query to DBforPostgreSQLFlexibleServer") + } + }) + }) + + t.Run("Get_WithPrivateEndpointLink", func(t *testing.T) { + peID := "/subscriptions/test-subscription/resourceGroups/other-rg/providers/Microsoft.Network/privateEndpoints/test-pe" + conn := createAzureDBforPostgreSQLFlexibleServerPrivateEndpointConnection(connectionName, peID) + + mockClient := mocks.NewMockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, connectionName).Return( + armpostgresqlflexibleservers.PrivateEndpointConnectionsClientGetResponse{ + PrivateEndpointConnection: *conn, + }, nil) + + testClient := &testDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient{MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewDBforPostgreSQLFlexibleServerPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, connectionName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + foundPrivateEndpoint := false + for _, lq := range sdpItem.GetLinkedItemQueries() { + if lq.GetQuery().GetType() == azureshared.NetworkPrivateEndpoint.String() { + foundPrivateEndpoint = true + if lq.GetQuery().GetQuery() != "test-pe" { + t.Errorf("Expected NetworkPrivateEndpoint query 'test-pe', got %s", lq.GetQuery().GetQuery()) + } + } + } + if !foundPrivateEndpoint { + t.Error("Expected linked query to NetworkPrivateEndpoint when PrivateEndpoint ID is set") + } + }) + + t.Run("Search", func(t *testing.T) { + conn1 := createAzureDBforPostgreSQLFlexibleServerPrivateEndpointConnection("pec-1", "") + conn2 := createAzureDBforPostgreSQLFlexibleServerPrivateEndpointConnection("pec-2", "") + + mockClient := mocks.NewMockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient(ctrl) + mockPager := &mockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsPager{ + pages: []armpostgresqlflexibleservers.PrivateEndpointConnectionsClientListByServerResponse{ + { + PrivateEndpointConnectionList: armpostgresqlflexibleservers.PrivateEndpointConnectionList{ + Value: []*armpostgresqlflexibleservers.PrivateEndpointConnection{conn1, conn2}, + }, + }, + }, + } + testClient := &testDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient{ + MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewDBforPostgreSQLFlexibleServerPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + items, qErr := wrapper.Search(ctx, subscriptionID+"."+resourceGroup, serverName) + if qErr != nil { + t.Fatalf("Search failed: %v", qErr) + } + if len(items) != 2 { + t.Errorf("Expected 2 items, got %d", len(items)) + } + for _, item := range items { + if item.GetType() != azureshared.DBforPostgreSQLFlexibleServerPrivateEndpointConnection.String() { + t.Errorf("Expected type %s, got %s", azureshared.DBforPostgreSQLFlexibleServerPrivateEndpointConnection, item.GetType()) + } + } + }) + + t.Run("Search_NilNameSkipped", func(t *testing.T) { + validConn := createAzureDBforPostgreSQLFlexibleServerPrivateEndpointConnection("valid-pec", "") + + mockClient := mocks.NewMockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient(ctrl) + mockPager := &mockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsPager{ + pages: []armpostgresqlflexibleservers.PrivateEndpointConnectionsClientListByServerResponse{ + { + PrivateEndpointConnectionList: armpostgresqlflexibleservers.PrivateEndpointConnectionList{ + Value: []*armpostgresqlflexibleservers.PrivateEndpointConnection{ + nil, + {Name: nil}, + validConn, + }, + }, + }, + }, + } + testClient := &testDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient{ + MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewDBforPostgreSQLFlexibleServerPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + items, qErr := wrapper.Search(ctx, subscriptionID+"."+resourceGroup, serverName) + if qErr != nil { + t.Fatalf("Search failed: %v", qErr) + } + if len(items) != 1 { + t.Errorf("Expected 1 item (nil names skipped), got %d", len(items)) + } + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient(ctrl) + testClient := &testDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient{MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewDBforPostgreSQLFlexibleServerPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], serverName, true) + if qErr == nil { + t.Error("Expected error when query has only serverName") + } + }) + + t.Run("ErrorHandling", func(t *testing.T) { + expectedErr := errors.New("connection not found") + mockClient := mocks.NewMockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, serverName, connectionName).Return( + armpostgresqlflexibleservers.PrivateEndpointConnectionsClientGetResponse{}, expectedErr) + + testClient := &testDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient{MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewDBforPostgreSQLFlexibleServerPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(serverName, connectionName) + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Fatal("Expected error when Get fails") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + wrapper := manual.NewDBforPostgreSQLFlexibleServerPrivateEndpointConnection(nil, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + links := wrapper.PotentialLinks() + if !links[azureshared.DBforPostgreSQLFlexibleServer] { + t.Error("Expected PotentialLinks to include DBforPostgreSQLFlexibleServer") + } + if !links[azureshared.NetworkPrivateEndpoint] { + t.Error("Expected PotentialLinks to include NetworkPrivateEndpoint") + } + }) +} + +func createAzureDBforPostgreSQLFlexibleServerPrivateEndpointConnection(connectionName, privateEndpointID string) *armpostgresqlflexibleservers.PrivateEndpointConnection { + state := armpostgresqlflexibleservers.PrivateEndpointConnectionProvisioningStateSucceeded + conn := &armpostgresqlflexibleservers.PrivateEndpointConnection{ + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.DBforPostgreSQL/flexibleServers/test-pg-server/privateEndpointConnections/" + connectionName), + Name: new(connectionName), + Type: new("Microsoft.DBforPostgreSQL/flexibleServers/privateEndpointConnections"), + Properties: &armpostgresqlflexibleservers.PrivateEndpointConnectionProperties{ + ProvisioningState: &state, + }, + } + if privateEndpointID != "" { + conn.Properties.PrivateEndpoint = &armpostgresqlflexibleservers.PrivateEndpoint{ + ID: new(privateEndpointID), + } + } + return conn +} diff --git a/sources/azure/shared/mocks/mock_dbforpostgresql_flexible_server_private_endpoint_connection_client.go b/sources/azure/shared/mocks/mock_dbforpostgresql_flexible_server_private_endpoint_connection_client.go new file mode 100644 index 00000000..9c60a8a5 --- /dev/null +++ b/sources/azure/shared/mocks/mock_dbforpostgresql_flexible_server_private_endpoint_connection_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: dbforpostgresql-flexible-server-private-endpoint-connection-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_dbforpostgresql_flexible_server_private_endpoint_connection_client.go -package=mocks -source=dbforpostgresql-flexible-server-private-endpoint-connection-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armpostgresqlflexibleservers "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient is a mock of DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient interface. +type MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient struct { + ctrl *gomock.Controller + recorder *MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClientMockRecorder + isgomock struct{} +} + +// MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClientMockRecorder is the mock recorder for MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient. +type MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClientMockRecorder struct { + mock *MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient +} + +// NewMockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient creates a new mock instance. +func NewMockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient(ctrl *gomock.Controller) *MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient { + mock := &MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient{ctrl: ctrl} + mock.recorder = &MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient) EXPECT() *MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient) Get(ctx context.Context, resourceGroupName, serverName, privateEndpointConnectionName string) (armpostgresqlflexibleservers.PrivateEndpointConnectionsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, serverName, privateEndpointConnectionName) + ret0, _ := ret[0].(armpostgresqlflexibleservers.PrivateEndpointConnectionsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClientMockRecorder) Get(ctx, resourceGroupName, serverName, privateEndpointConnectionName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient)(nil).Get), ctx, resourceGroupName, serverName, privateEndpointConnectionName) +} + +// ListByServer mocks base method. +func (m *MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient) ListByServer(ctx context.Context, resourceGroupName, serverName string) clients.DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByServer", ctx, resourceGroupName, serverName) + ret0, _ := ret[0].(clients.DBforPostgreSQLFlexibleServerPrivateEndpointConnectionsPager) + return ret0 +} + +// ListByServer indicates an expected call of ListByServer. +func (mr *MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClientMockRecorder) ListByServer(ctx, resourceGroupName, serverName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByServer", reflect.TypeOf((*MockDBforPostgreSQLFlexibleServerPrivateEndpointConnectionsClient)(nil).ListByServer), ctx, resourceGroupName, serverName) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 7723fea3..19277412 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -32,6 +32,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-sql-server-private-endpoint-connection": {"servers", "privateEndpointConnections"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Sql/servers/{serverName}/privateEndpointConnections/{connectionName}", "azure-dbforpostgresql-database": {"flexibleServers", "databases"}, // "/subscriptions/.../Microsoft.DBforPostgreSQL/flexibleServers/{server}/databases/{db}", "azure-dbforpostgresql-flexible-server-firewall-rule": {"flexibleServers", "firewallRules"}, // "/subscriptions/.../Microsoft.DBforPostgreSQL/flexibleServers/{server}/firewallRules/{rule}", + "azure-dbforpostgresql-flexible-server-private-endpoint-connection": {"flexibleServers", "privateEndpointConnections"}, // "/subscriptions/.../Microsoft.DBforPostgreSQL/flexibleServers/{server}/privateEndpointConnections/{connectionName}", "azure-keyvault-secret": {"vaults", "secrets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{vaultName}/secrets/{secretName}", "azure-keyvault-key": {"vaults", "keys"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{vaultName}/keys/{keyName}", "azure-authorization-role-assignment": {"roleAssignments"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Authorization/roleAssignments/{roleAssignmentName}", From b72b71b9c20b39fe69d09f1dc5e98381153e15e9 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Tue, 3 Mar 2026 18:09:07 +0100 Subject: [PATCH 49/74] [ENG-2678] Add OpenTofu registry links to customer docs (#4091) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Updated customer-facing Terraform/OpenTofu docs to reflect that the Overmind provider and module are now published on the OpenTofu Registry - Verified `tofu init` installs both with proper GPG signature validation (no "Signature validation was skipped" warning) ## Linear Ticket - **Ticket**: [ENG-2678](https://linear.app/overmind/issue/ENG-2678/do-opentofu-release-process-for-phase-5) — Do OpenTofu release process for Phase 5 - **Purpose**: Complete the OpenTofu Registry enrollment that was deferred from ENG-2673 (Phase 5: Copybara & Publishing) ## Changes Single file change to `docs.overmind.tech/docs/sources/aws/terraform.md`: - **Title**: "Configure with Terraform" -> "Configure with Terraform / OpenTofu" - **Intro**: Updated to mention both registries with links - **Quick Start**: Added `tofu init / plan / apply` commands alongside the existing Terraform commands - **Registry Links**: Replaced "coming soon" placeholder with actual OpenTofu Registry links for both the [provider](https://search.opentofu.org/provider/overmindtech/overmind) and [module](https://search.opentofu.org/module/overmindtech/aws-source/overmind) Made with [Cursor](https://cursor.com) --- > [!NOTE] > **Low Risk** > Low risk documentation-only change; updates links and command examples without affecting product code or infrastructure behavior. > > **Overview** > Updates the AWS source setup docs to explicitly cover **OpenTofu** alongside Terraform by renaming the page, adding `tofu init/plan/apply` quick-start commands, and noting `tofu apply` as an alternative. > > Replaces the OpenTofu Registry “coming soon” placeholder with live registry links for both the `overmindtech/overmind` provider and `overmindtech/aws-source` module, and updates the intro to reference availability on both registries. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0e8d5a3ad6941e815c0880f0285e287c400243c0. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: d0d6be741ae527dea3caab6b494a1350ac643c5e --- docs.overmind.tech/docs/sources/aws/terraform.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs.overmind.tech/docs/sources/aws/terraform.md b/docs.overmind.tech/docs/sources/aws/terraform.md index 5e780730..9cbe206a 100644 --- a/docs.overmind.tech/docs/sources/aws/terraform.md +++ b/docs.overmind.tech/docs/sources/aws/terraform.md @@ -1,9 +1,9 @@ --- -title: Configure with Terraform +title: Configure with Terraform / OpenTofu 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/). +The Overmind Terraform module configures an AWS account for Overmind infrastructure discovery in a single `terraform apply` (or `tofu 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 and provider are available on both the [Terraform Registry](https://registry.terraform.io/modules/overmindtech/aws-source/overmind) and the [OpenTofu Registry](https://search.opentofu.org/module/overmindtech/aws-source/overmind). ## Prerequisites @@ -44,6 +44,15 @@ terraform plan terraform apply ``` +Or with OpenTofu: + +```bash +export OVERMIND_API_KEY="your-api-key" +tofu init +tofu plan +tofu apply +``` + ## Authentication ### Overmind Provider @@ -157,7 +166,7 @@ After `terraform apply` completes: ## 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 +- **OpenTofu Registry**: [overmindtech/overmind provider](https://search.opentofu.org/provider/overmindtech/overmind) | [overmindtech/aws-source module](https://search.opentofu.org/module/overmindtech/aws-source/overmind) ## Troubleshooting From b6378da1b96bb20a9fc2f3408aa41b31684a3d7e Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:42:11 +0000 Subject: [PATCH 50/74] Eng 2881 create keyvaultmanagedhsmprivateendpointconnection adapter (#4093) image --- > [!NOTE] > **Medium Risk** > Adds a new Azure adapter plus SDK client initialization to the main `Adapters` wiring, which can affect source startup and discovery behavior if the new client or paging logic misbehaves. Changes are additive/read-only but touch the adapter registration path used for all runs. > > **Overview** > Adds discovery support for **Azure Key Vault Managed HSM private endpoint connections** via a new searchable wrapper (`NewKeyVaultManagedHSMPrivateEndpointConnection`) with `Get`, `Search`, and streaming search. > > Introduces a small Azure SDK client wrapper interface (`KeyVaultManagedHSMPrivateEndpointConnectionsClient`) and generated GoMock, plus unit tests covering lookup behavior, paging, error handling, and linked-item generation (links to `KeyVaultManagedHSM`, `NetworkPrivateEndpoint`, and `ManagedIdentityUserAssignedIdentity`) and health mapping from provisioning state. > > Wires the new adapter into `manual/adapters.go` by creating `armkeyvault.NewMHSMPrivateEndpointConnectionsClient` and registering the adapter in both real and placeholder adapter lists, and updates `shared/utils.go` resource-ID path key mappings for `azure-keyvault-managed-hsm-private-endpoint-connection`. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f66a098752cd4c197fe60692a50fec9aab7c21b4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 40509e2c92720b77f8358877ec8d304145ec8b80 --- ...-hsm-private-endpoint-connection-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + ...managed-hsm-private-endpoint-connection.go | 260 ++++++++++++ ...ed-hsm-private-endpoint-connection_test.go | 370 ++++++++++++++++++ ..._hsm_private_endpoint_connection_client.go | 72 ++++ sources/azure/shared/utils.go | 1 + 6 files changed, 748 insertions(+) create mode 100644 sources/azure/clients/keyvault-managed-hsm-private-endpoint-connection-client.go create mode 100644 sources/azure/manual/keyvault-managed-hsm-private-endpoint-connection.go create mode 100644 sources/azure/manual/keyvault-managed-hsm-private-endpoint-connection_test.go create mode 100644 sources/azure/shared/mocks/mock_keyvault_managed_hsm_private_endpoint_connection_client.go diff --git a/sources/azure/clients/keyvault-managed-hsm-private-endpoint-connection-client.go b/sources/azure/clients/keyvault-managed-hsm-private-endpoint-connection-client.go new file mode 100644 index 00000000..0fef287c --- /dev/null +++ b/sources/azure/clients/keyvault-managed-hsm-private-endpoint-connection-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" +) + +//go:generate mockgen -destination=../shared/mocks/mock_keyvault_managed_hsm_private_endpoint_connection_client.go -package=mocks -source=keyvault-managed-hsm-private-endpoint-connection-client.go + +// KeyVaultManagedHSMPrivateEndpointConnectionsPager is a type alias for the generic Pager interface with MHSM private endpoint connection list response type. +type KeyVaultManagedHSMPrivateEndpointConnectionsPager = Pager[armkeyvault.MHSMPrivateEndpointConnectionsClientListByResourceResponse] + +// KeyVaultManagedHSMPrivateEndpointConnectionsClient is an interface for interacting with Azure Key Vault Managed HSM private endpoint connections. +type KeyVaultManagedHSMPrivateEndpointConnectionsClient interface { + Get(ctx context.Context, resourceGroupName string, hsmName string, privateEndpointConnectionName string) (armkeyvault.MHSMPrivateEndpointConnectionsClientGetResponse, error) + ListByResource(ctx context.Context, resourceGroupName string, hsmName string) KeyVaultManagedHSMPrivateEndpointConnectionsPager +} + +type keyvaultManagedHSMPrivateEndpointConnectionsClient struct { + client *armkeyvault.MHSMPrivateEndpointConnectionsClient +} + +func (c *keyvaultManagedHSMPrivateEndpointConnectionsClient) Get(ctx context.Context, resourceGroupName string, hsmName string, privateEndpointConnectionName string) (armkeyvault.MHSMPrivateEndpointConnectionsClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, hsmName, privateEndpointConnectionName, nil) +} + +func (c *keyvaultManagedHSMPrivateEndpointConnectionsClient) ListByResource(ctx context.Context, resourceGroupName string, hsmName string) KeyVaultManagedHSMPrivateEndpointConnectionsPager { + return c.client.NewListByResourcePager(resourceGroupName, hsmName, nil) +} + +// NewKeyVaultManagedHSMPrivateEndpointConnectionsClient creates a new KeyVaultManagedHSMPrivateEndpointConnectionsClient from the Azure SDK client. +func NewKeyVaultManagedHSMPrivateEndpointConnectionsClient(client *armkeyvault.MHSMPrivateEndpointConnectionsClient) KeyVaultManagedHSMPrivateEndpointConnectionsClient { + return &keyvaultManagedHSMPrivateEndpointConnectionsClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 6925095f..9f80db28 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -235,6 +235,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create managed hsms client: %w", err) } + mhsmPrivateEndpointConnectionsClient, err := armkeyvault.NewMHSMPrivateEndpointConnectionsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create MHSM private endpoint connections client: %w", err) + } + sqlServersClient, err := armsql.NewServersClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create sql servers client: %w", err) @@ -465,6 +470,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewManagedHSMsClient(managedHSMsClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewKeyVaultManagedHSMPrivateEndpointConnection( + clients.NewKeyVaultManagedHSMPrivateEndpointConnectionsClient(mhsmPrivateEndpointConnectionsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewDBforPostgreSQLDatabase( clients.NewPostgreSQLDatabasesClient(postgreSQLDatabasesClient), resourceGroupScopes, @@ -664,6 +673,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewDocumentDBPrivateEndpointConnection(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewKeyVaultVault(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewKeyVaultManagedHSM(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewKeyVaultManagedHSMPrivateEndpointConnection(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDBforPostgreSQLDatabase(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkPublicIPAddress(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkLoadBalancer(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/keyvault-managed-hsm-private-endpoint-connection.go b/sources/azure/manual/keyvault-managed-hsm-private-endpoint-connection.go new file mode 100644 index 00000000..70d80c38 --- /dev/null +++ b/sources/azure/manual/keyvault-managed-hsm-private-endpoint-connection.go @@ -0,0 +1,260 @@ +package manual + +import ( + "context" + "errors" + "strings" + + "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" + "github.com/overmindtech/cli/sources/shared" +) + +var KeyVaultManagedHSMPrivateEndpointConnectionLookupByName = shared.NewItemTypeLookup("name", azureshared.KeyVaultManagedHSMPrivateEndpointConnection) + +type keyvaultManagedHSMPrivateEndpointConnectionWrapper struct { + client clients.KeyVaultManagedHSMPrivateEndpointConnectionsClient + + *azureshared.MultiResourceGroupBase +} + +// NewKeyVaultManagedHSMPrivateEndpointConnection returns a SearchableWrapper for Azure Key Vault Managed HSM private endpoint connections. +func NewKeyVaultManagedHSMPrivateEndpointConnection(client clients.KeyVaultManagedHSMPrivateEndpointConnectionsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &keyvaultManagedHSMPrivateEndpointConnectionWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_SECURITY, + azureshared.KeyVaultManagedHSMPrivateEndpointConnection, + ), + } +} + +func (s keyvaultManagedHSMPrivateEndpointConnectionWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 2 query parts: hsmName and privateEndpointConnectionName", + Scope: scope, + ItemType: s.Type(), + } + } + hsmName := queryParts[0] + connectionName := queryParts[1] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + resp, err := s.client.Get(ctx, rgScope.ResourceGroup, hsmName, connectionName) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + item, sdpErr := s.azureMHSMPrivateEndpointConnectionToSDPItem(&resp.MHSMPrivateEndpointConnection, hsmName, connectionName, scope) + if sdpErr != nil { + return nil, sdpErr + } + return item, nil +} + +func (s keyvaultManagedHSMPrivateEndpointConnectionWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + KeyVaultManagedHSMsLookupByName, + KeyVaultManagedHSMPrivateEndpointConnectionLookupByName, + } +} + +func (s keyvaultManagedHSMPrivateEndpointConnectionWrapper) Search(ctx context.Context, scope string, queryParts ...string) ([]*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Search requires 1 query part: hsmName", + Scope: scope, + ItemType: s.Type(), + } + } + hsmName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + pager := s.client.ListByResource(ctx, rgScope.ResourceGroup, hsmName) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + for _, conn := range page.Value { + if conn == nil || conn.Name == nil { + continue + } + + item, sdpErr := s.azureMHSMPrivateEndpointConnectionToSDPItem(conn, hsmName, *conn.Name, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (s keyvaultManagedHSMPrivateEndpointConnectionWrapper) 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: hsmName"), scope, s.Type())) + return + } + hsmName := queryParts[0] + + rgScope, err := s.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + pager := s.client.ListByResource(ctx, rgScope.ResourceGroup, hsmName) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, s.Type())) + return + } + for _, conn := range page.Value { + if conn == nil || conn.Name == nil { + continue + } + item, sdpErr := s.azureMHSMPrivateEndpointConnectionToSDPItem(conn, hsmName, *conn.Name, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (s keyvaultManagedHSMPrivateEndpointConnectionWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + { + KeyVaultManagedHSMsLookupByName, + }, + } +} + +func (s keyvaultManagedHSMPrivateEndpointConnectionWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.KeyVaultManagedHSM: true, + azureshared.NetworkPrivateEndpoint: true, + azureshared.ManagedIdentityUserAssignedIdentity: true, + } +} + +func (s keyvaultManagedHSMPrivateEndpointConnectionWrapper) azureMHSMPrivateEndpointConnectionToSDPItem(conn *armkeyvault.MHSMPrivateEndpointConnection, hsmName, connectionName, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(conn, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + err = attributes.Set("uniqueAttr", shared.CompositeLookupKey(hsmName, connectionName)) + if err != nil { + return nil, azureshared.QueryError(err, scope, s.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.KeyVaultManagedHSMPrivateEndpointConnection.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(conn.Tags), + } + + // Health from provisioning state + if conn.Properties != nil && conn.Properties.ProvisioningState != nil { + state := strings.ToLower(string(*conn.Properties.ProvisioningState)) + switch state { + case "succeeded": + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case "creating", "updating", "deleting": + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case "failed": + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + // Link to parent Key Vault Managed HSM + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.KeyVaultManagedHSM.String(), + Method: sdp.QueryMethod_GET, + Query: hsmName, + Scope: scope, + }, + }) + + // Link to Network Private Endpoint when present (may be in different resource group) + if conn.Properties != nil && conn.Properties.PrivateEndpoint != nil && conn.Properties.PrivateEndpoint.ID != nil { + peID := *conn.Properties.PrivateEndpoint.ID + peName := azureshared.ExtractResourceName(peID) + if peName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(peID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPrivateEndpoint.String(), + Method: sdp.QueryMethod_GET, + Query: peName, + Scope: linkedScope, + }, + }) + } + } + + // Link to User Assigned Managed Identities (same pattern as KeyVaultManagedHSM adapter) + // User Assigned Identities can be in a different resource group than the Managed HSM. + if conn.Identity != nil && conn.Identity.UserAssignedIdentities != nil { + for identityResourceID := range conn.Identity.UserAssignedIdentities { + identityName := azureshared.ExtractResourceName(identityResourceID) + if identityName != "" { + linkedScope := scope + if extractedScope := azureshared.ExtractScopeFromResourceID(identityResourceID); extractedScope != "" { + linkedScope = extractedScope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ManagedIdentityUserAssignedIdentity.String(), + Method: sdp.QueryMethod_GET, + Query: identityName, + Scope: linkedScope, + }, + }) + } + } + } + + return sdpItem, nil +} + +func (s keyvaultManagedHSMPrivateEndpointConnectionWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.KeyVault/managedHSMs/privateEndpointConnections/read", + } +} + +func (s keyvaultManagedHSMPrivateEndpointConnectionWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/keyvault-managed-hsm-private-endpoint-connection_test.go b/sources/azure/manual/keyvault-managed-hsm-private-endpoint-connection_test.go new file mode 100644 index 00000000..2f727524 --- /dev/null +++ b/sources/azure/manual/keyvault-managed-hsm-private-endpoint-connection_test.go @@ -0,0 +1,370 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" + "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" +) + +type mockKeyVaultManagedHSMPrivateEndpointConnectionsPager struct { + pages []armkeyvault.MHSMPrivateEndpointConnectionsClientListByResourceResponse + index int +} + +func (m *mockKeyVaultManagedHSMPrivateEndpointConnectionsPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockKeyVaultManagedHSMPrivateEndpointConnectionsPager) NextPage(ctx context.Context) (armkeyvault.MHSMPrivateEndpointConnectionsClientListByResourceResponse, error) { + if m.index >= len(m.pages) { + return armkeyvault.MHSMPrivateEndpointConnectionsClientListByResourceResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +type testKeyVaultManagedHSMPrivateEndpointConnectionsClient struct { + *mocks.MockKeyVaultManagedHSMPrivateEndpointConnectionsClient + pager clients.KeyVaultManagedHSMPrivateEndpointConnectionsPager +} + +func (t *testKeyVaultManagedHSMPrivateEndpointConnectionsClient) ListByResource(ctx context.Context, resourceGroupName, hsmName string) clients.KeyVaultManagedHSMPrivateEndpointConnectionsPager { + return t.pager +} + +func TestKeyVaultManagedHSMPrivateEndpointConnection(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + hsmName := "test-hsm" + connectionName := "test-pec" + + t.Run("Get", func(t *testing.T) { + conn := createAzureMHSMPrivateEndpointConnection(connectionName, "") + + mockClient := mocks.NewMockKeyVaultManagedHSMPrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, hsmName, connectionName).Return( + armkeyvault.MHSMPrivateEndpointConnectionsClientGetResponse{ + MHSMPrivateEndpointConnection: *conn, + }, nil) + + testClient := &testKeyVaultManagedHSMPrivateEndpointConnectionsClient{MockKeyVaultManagedHSMPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewKeyVaultManagedHSMPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(hsmName, connectionName) + 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.KeyVaultManagedHSMPrivateEndpointConnection.String() { + t.Errorf("Expected type %s, got %s", azureshared.KeyVaultManagedHSMPrivateEndpointConnection, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != shared.CompositeLookupKey(hsmName, connectionName) { + t.Errorf("Expected unique attribute value %s, got %s", shared.CompositeLookupKey(hsmName, connectionName), sdpItem.UniqueAttributeValue()) + } + + if sdpItem.GetScope() != subscriptionID+"."+resourceGroup { + t.Errorf("Expected scope %s, got %s", subscriptionID+"."+resourceGroup, sdpItem.GetScope()) + } + + if err := sdpItem.Validate(); err != nil { + t.Fatalf("Expected no validation error, got: %v", err) + } + + t.Run("StaticTests", func(t *testing.T) { + linkedQueries := sdpItem.GetLinkedItemQueries() + if len(linkedQueries) < 1 { + t.Fatalf("Expected at least 1 linked query, got: %d", len(linkedQueries)) + } + + foundKeyVaultManagedHSM := false + for _, lq := range linkedQueries { + if lq.GetQuery().GetType() == azureshared.KeyVaultManagedHSM.String() { + foundKeyVaultManagedHSM = true + if lq.GetQuery().GetMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected KeyVaultManagedHSM link method GET, got %v", lq.GetQuery().GetMethod()) + } + if lq.GetQuery().GetQuery() != hsmName { + t.Errorf("Expected KeyVaultManagedHSM query %s, got %s", hsmName, lq.GetQuery().GetQuery()) + } + } + } + if !foundKeyVaultManagedHSM { + t.Error("Expected linked query to KeyVaultManagedHSM") + } + }) + }) + + t.Run("Get_WithPrivateEndpointLink", func(t *testing.T) { + peID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/privateEndpoints/test-pe" + conn := createAzureMHSMPrivateEndpointConnection(connectionName, peID) + + mockClient := mocks.NewMockKeyVaultManagedHSMPrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, hsmName, connectionName).Return( + armkeyvault.MHSMPrivateEndpointConnectionsClientGetResponse{ + MHSMPrivateEndpointConnection: *conn, + }, nil) + + testClient := &testKeyVaultManagedHSMPrivateEndpointConnectionsClient{MockKeyVaultManagedHSMPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewKeyVaultManagedHSMPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(hsmName, connectionName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + foundPrivateEndpoint := false + for _, lq := range sdpItem.GetLinkedItemQueries() { + if lq.GetQuery().GetType() == azureshared.NetworkPrivateEndpoint.String() { + foundPrivateEndpoint = true + if lq.GetQuery().GetQuery() != "test-pe" { + t.Errorf("Expected NetworkPrivateEndpoint query 'test-pe', got %s", lq.GetQuery().GetQuery()) + } + break + } + } + if !foundPrivateEndpoint { + t.Error("Expected linked query to NetworkPrivateEndpoint when PrivateEndpoint ID is set") + } + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockKeyVaultManagedHSMPrivateEndpointConnectionsClient(ctrl) + testClient := &testKeyVaultManagedHSMPrivateEndpointConnectionsClient{MockKeyVaultManagedHSMPrivateEndpointConnectionsClient: mockClient} + + wrapper := manual.NewKeyVaultManagedHSMPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], hsmName, true) + if qErr == nil { + t.Error("Expected error when providing insufficient query parts, but got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + conn1 := createAzureMHSMPrivateEndpointConnection("pec-1", "") + conn2 := createAzureMHSMPrivateEndpointConnection("pec-2", "") + + mockClient := mocks.NewMockKeyVaultManagedHSMPrivateEndpointConnectionsClient(ctrl) + mockPager := &mockKeyVaultManagedHSMPrivateEndpointConnectionsPager{ + pages: []armkeyvault.MHSMPrivateEndpointConnectionsClientListByResourceResponse{ + { + MHSMPrivateEndpointConnectionsListResult: armkeyvault.MHSMPrivateEndpointConnectionsListResult{ + Value: []*armkeyvault.MHSMPrivateEndpointConnection{conn1, conn2}, + }, + }, + }, + } + + testClient := &testKeyVaultManagedHSMPrivateEndpointConnectionsClient{ + MockKeyVaultManagedHSMPrivateEndpointConnectionsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewKeyVaultManagedHSMPrivateEndpointConnection(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, wrapper.Scopes()[0], hsmName, 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.Fatalf("Expected no validation error, got: %v", err) + } + if item.GetType() != azureshared.KeyVaultManagedHSMPrivateEndpointConnection.String() { + t.Errorf("Expected type %s, got %s", azureshared.KeyVaultManagedHSMPrivateEndpointConnection, item.GetType()) + } + } + }) + + t.Run("Search_NilNameSkipped", func(t *testing.T) { + validConn := createAzureMHSMPrivateEndpointConnection("valid-pec", "") + + mockClient := mocks.NewMockKeyVaultManagedHSMPrivateEndpointConnectionsClient(ctrl) + mockPager := &mockKeyVaultManagedHSMPrivateEndpointConnectionsPager{ + pages: []armkeyvault.MHSMPrivateEndpointConnectionsClientListByResourceResponse{ + { + MHSMPrivateEndpointConnectionsListResult: armkeyvault.MHSMPrivateEndpointConnectionsListResult{ + Value: []*armkeyvault.MHSMPrivateEndpointConnection{ + {Name: nil}, + validConn, + }, + }, + }, + }, + } + + testClient := &testKeyVaultManagedHSMPrivateEndpointConnectionsClient{ + MockKeyVaultManagedHSMPrivateEndpointConnectionsClient: mockClient, + pager: mockPager, + } + + wrapper := manual.NewKeyVaultManagedHSMPrivateEndpointConnection(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, wrapper.Scopes()[0], hsmName, 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)) + } + if sdpItems[0].UniqueAttributeValue() != shared.CompositeLookupKey(hsmName, "valid-pec") { + t.Errorf("Expected unique value %s, got %s", shared.CompositeLookupKey(hsmName, "valid-pec"), sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("Search_InvalidQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockKeyVaultManagedHSMPrivateEndpointConnectionsClient(ctrl) + testClient := &testKeyVaultManagedHSMPrivateEndpointConnectionsClient{MockKeyVaultManagedHSMPrivateEndpointConnectionsClient: mockClient} + + wrapper := manual.NewKeyVaultManagedHSMPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + _, qErr := wrapper.Search(ctx, wrapper.Scopes()[0]) + if qErr == nil { + t.Error("Expected error when providing no query parts, but got nil") + } + }) + + t.Run("ErrorHandling_Get", func(t *testing.T) { + expectedErr := errors.New("private endpoint connection not found") + + mockClient := mocks.NewMockKeyVaultManagedHSMPrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, hsmName, "nonexistent-pec").Return( + armkeyvault.MHSMPrivateEndpointConnectionsClientGetResponse{}, expectedErr) + + testClient := &testKeyVaultManagedHSMPrivateEndpointConnectionsClient{MockKeyVaultManagedHSMPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewKeyVaultManagedHSMPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(hsmName, "nonexistent-pec") + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when getting non-existent private endpoint connection, but got nil") + } + }) + + t.Run("Get_WithUserAssignedIdentityLink", func(t *testing.T) { + identityID := "/subscriptions/" + subscriptionID + "/resourceGroups/identity-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/test-identity" + conn := createAzureMHSMPrivateEndpointConnectionWithIdentity(connectionName, "", identityID) + + mockClient := mocks.NewMockKeyVaultManagedHSMPrivateEndpointConnectionsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, hsmName, connectionName).Return( + armkeyvault.MHSMPrivateEndpointConnectionsClientGetResponse{ + MHSMPrivateEndpointConnection: *conn, + }, nil) + + testClient := &testKeyVaultManagedHSMPrivateEndpointConnectionsClient{MockKeyVaultManagedHSMPrivateEndpointConnectionsClient: mockClient} + wrapper := manual.NewKeyVaultManagedHSMPrivateEndpointConnection(testClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + query := shared.CompositeLookupKey(hsmName, connectionName) + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + foundIdentity := false + for _, lq := range sdpItem.GetLinkedItemQueries() { + if lq.GetQuery().GetType() == azureshared.ManagedIdentityUserAssignedIdentity.String() { + foundIdentity = true + if lq.GetQuery().GetQuery() != "test-identity" { + t.Errorf("Expected ManagedIdentityUserAssignedIdentity query 'test-identity', got %s", lq.GetQuery().GetQuery()) + } + if lq.GetQuery().GetScope() != subscriptionID+".identity-rg" { + t.Errorf("Expected scope %s.identity-rg for identity in different RG, got %s", subscriptionID, lq.GetQuery().GetScope()) + } + } + } + if !foundIdentity { + t.Error("Expected linked query to ManagedIdentityUserAssignedIdentity when Identity.UserAssignedIdentities is set") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + wrapper := manual.NewKeyVaultManagedHSMPrivateEndpointConnection(nil, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + links := wrapper.PotentialLinks() + if !links[azureshared.KeyVaultManagedHSM] { + t.Error("Expected KeyVaultManagedHSM in PotentialLinks") + } + if !links[azureshared.NetworkPrivateEndpoint] { + t.Error("Expected NetworkPrivateEndpoint in PotentialLinks") + } + if !links[azureshared.ManagedIdentityUserAssignedIdentity] { + t.Error("Expected ManagedIdentityUserAssignedIdentity in PotentialLinks") + } + }) +} + +func createAzureMHSMPrivateEndpointConnection(connectionName, privateEndpointID string) *armkeyvault.MHSMPrivateEndpointConnection { + return createAzureMHSMPrivateEndpointConnectionWithIdentity(connectionName, privateEndpointID, "") +} + +func createAzureMHSMPrivateEndpointConnectionWithIdentity(connectionName, privateEndpointID, identityResourceID string) *armkeyvault.MHSMPrivateEndpointConnection { + state := armkeyvault.PrivateEndpointConnectionProvisioningStateSucceeded + conn := &armkeyvault.MHSMPrivateEndpointConnection{ + ID: new("/subscriptions/test-subscription/resourceGroups/test-rg/providers/Microsoft.KeyVault/managedHSMs/test-hsm/privateEndpointConnections/" + connectionName), + Name: new(connectionName), + Type: new("Microsoft.KeyVault/managedHSMs/privateEndpointConnections"), + Properties: &armkeyvault.MHSMPrivateEndpointConnectionProperties{ + ProvisioningState: &state, + }, + } + if privateEndpointID != "" { + conn.Properties.PrivateEndpoint = &armkeyvault.MHSMPrivateEndpoint{ + ID: new(privateEndpointID), + } + } + if identityResourceID != "" { + conn.Identity = &armkeyvault.ManagedServiceIdentity{ + Type: new(armkeyvault.ManagedServiceIdentityTypeUserAssigned), + UserAssignedIdentities: map[string]*armkeyvault.UserAssignedIdentity{ + identityResourceID: {}, + }, + } + } + return conn +} diff --git a/sources/azure/shared/mocks/mock_keyvault_managed_hsm_private_endpoint_connection_client.go b/sources/azure/shared/mocks/mock_keyvault_managed_hsm_private_endpoint_connection_client.go new file mode 100644 index 00000000..6b24ae39 --- /dev/null +++ b/sources/azure/shared/mocks/mock_keyvault_managed_hsm_private_endpoint_connection_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: keyvault-managed-hsm-private-endpoint-connection-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_keyvault_managed_hsm_private_endpoint_connection_client.go -package=mocks -source=keyvault-managed-hsm-private-endpoint-connection-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armkeyvault "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockKeyVaultManagedHSMPrivateEndpointConnectionsClient is a mock of KeyVaultManagedHSMPrivateEndpointConnectionsClient interface. +type MockKeyVaultManagedHSMPrivateEndpointConnectionsClient struct { + ctrl *gomock.Controller + recorder *MockKeyVaultManagedHSMPrivateEndpointConnectionsClientMockRecorder + isgomock struct{} +} + +// MockKeyVaultManagedHSMPrivateEndpointConnectionsClientMockRecorder is the mock recorder for MockKeyVaultManagedHSMPrivateEndpointConnectionsClient. +type MockKeyVaultManagedHSMPrivateEndpointConnectionsClientMockRecorder struct { + mock *MockKeyVaultManagedHSMPrivateEndpointConnectionsClient +} + +// NewMockKeyVaultManagedHSMPrivateEndpointConnectionsClient creates a new mock instance. +func NewMockKeyVaultManagedHSMPrivateEndpointConnectionsClient(ctrl *gomock.Controller) *MockKeyVaultManagedHSMPrivateEndpointConnectionsClient { + mock := &MockKeyVaultManagedHSMPrivateEndpointConnectionsClient{ctrl: ctrl} + mock.recorder = &MockKeyVaultManagedHSMPrivateEndpointConnectionsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockKeyVaultManagedHSMPrivateEndpointConnectionsClient) EXPECT() *MockKeyVaultManagedHSMPrivateEndpointConnectionsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockKeyVaultManagedHSMPrivateEndpointConnectionsClient) Get(ctx context.Context, resourceGroupName, hsmName, privateEndpointConnectionName string) (armkeyvault.MHSMPrivateEndpointConnectionsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, hsmName, privateEndpointConnectionName) + ret0, _ := ret[0].(armkeyvault.MHSMPrivateEndpointConnectionsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockKeyVaultManagedHSMPrivateEndpointConnectionsClientMockRecorder) Get(ctx, resourceGroupName, hsmName, privateEndpointConnectionName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockKeyVaultManagedHSMPrivateEndpointConnectionsClient)(nil).Get), ctx, resourceGroupName, hsmName, privateEndpointConnectionName) +} + +// ListByResource mocks base method. +func (m *MockKeyVaultManagedHSMPrivateEndpointConnectionsClient) ListByResource(ctx context.Context, resourceGroupName, hsmName string) clients.KeyVaultManagedHSMPrivateEndpointConnectionsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListByResource", ctx, resourceGroupName, hsmName) + ret0, _ := ret[0].(clients.KeyVaultManagedHSMPrivateEndpointConnectionsPager) + return ret0 +} + +// ListByResource indicates an expected call of ListByResource. +func (mr *MockKeyVaultManagedHSMPrivateEndpointConnectionsClientMockRecorder) ListByResource(ctx, resourceGroupName, hsmName any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListByResource", reflect.TypeOf((*MockKeyVaultManagedHSMPrivateEndpointConnectionsClient)(nil).ListByResource), ctx, resourceGroupName, hsmName) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index 19277412..a6e3b09c 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -35,6 +35,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-dbforpostgresql-flexible-server-private-endpoint-connection": {"flexibleServers", "privateEndpointConnections"}, // "/subscriptions/.../Microsoft.DBforPostgreSQL/flexibleServers/{server}/privateEndpointConnections/{connectionName}", "azure-keyvault-secret": {"vaults", "secrets"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{vaultName}/secrets/{secretName}", "azure-keyvault-key": {"vaults", "keys"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{vaultName}/keys/{keyName}", + "azure-keyvault-managed-hsm-private-endpoint-connection": {"managedHSMs", "privateEndpointConnections"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.KeyVault/managedHSMs/{name}/privateEndpointConnections/{connectionName}", "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}", From 894d4370bf8e01edebd6b05d5390ab663b622730 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Wed, 4 Mar 2026 00:00:13 +0100 Subject: [PATCH 51/74] [ENG-2943] Switch to overmindtech/otelpgx fork (no acquire/prepare spans) (#4103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Switch from `exaring/otelpgx` to `overmindtech/otelpgx` fork which removes `pool.acquire` and `prepare` span creation while preserving all metrics - Prepare duration is now recorded as a `pgx.prepare.duration` attribute (Int64, ms) on the parent query span - Remove the now-unnecessary `pool.acquire` sampling rule from `OvermindSampler` ## Linear Ticket - **Ticket**: [ENG-2943](https://linear.app/overmind/issue/ENG-2943/fork-otelpgx-remove-poolacquire-and-prepare-spans) — Fork otelpgx: remove pool.acquire and prepare spans - **Purpose**: Reduce trace noise by eliminating low-value `pool.acquire` and `prepare` child spans, while preserving `db.client.operation.duration` metrics for both operations - **Related**: [ENG-2941](https://linear.app/overmind/issue/ENG-2941) — complementary to the OTEL collector batch size fix ## Changes | File | Change | | --- | --- | | `go.mod` / `go.sum` | `exaring/otelpgx v0.10.0` replaced with `overmindtech/otelpgx` (commit `65bf101`) | | `go/dbkit/connect.go` | Import path updated to `github.com/overmindtech/otelpgx` | | `go/tracing/main.go` | Removed `pool.acquire` sampling rule from `NewOvermindSampler` (no longer needed) | The fork itself ([overmindtech/otelpgx#2](https://github.com/overmindtech/otelpgx/pull/2)) contains the functional changes to otelpgx. ## Deviations from Approved Plan Implementation matches the approved plan -- no material deviations. All seven parts were implemented as described: 1. Fork created at `overmindtech/otelpgx` with two commits (functionality + rename) 2. `pool.acquire` span removed, metrics preserved, rationale documented 3. `prepare` span removed, `pgx.prepare.duration` attribute added to parent query span 4. Unused code cleaned up from simplified methods 5. Two tests added (`TestTraceAcquire_NoSpan`, `TestTracePrepare_NoSpan_SetsAttribute`) 6. Main repo updated (this PR) 7. Upstream PR suggestion deferred to post-validation (as planned) Made with [Cursor](https://cursor.com) --- > [!NOTE] > **Medium Risk** > Moderate risk because it changes database OpenTelemetry instrumentation and sampling rules, which can affect trace volume/shape and observability expectations, but it is otherwise a small, localized dependency swap. > > **Overview** > Switches PostgreSQL tracing from `github.com/exaring/otelpgx` to the `github.com/overmindtech/otelpgx` fork, updating `dbkit.Connect` to use the new import and bumping module sums accordingly. > > Simplifies tracing sampling by removing the special-case sampler for `pool.acquire` spans (and the now-unused `SpanNameMatcher` helper), reflecting the fork’s reduced span creation. Adds a `//nolint:staticcheck` annotation for the Kubernetes `v1.Endpoints` adapter to suppress deprecation warnings while `EndpointSlice` migration is pending. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2c0f40d231fdad6a06ce4080b1b834cfed6dce42. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: cd50535a27c2563fb4dd32592182711892177b7d --- go.mod | 2 +- go.sum | 8 ++------ go/tracing/main.go | 11 ----------- k8s-source/adapters/endpoints.go | 1 + 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 03fde2ef..ac79e5e9 100644 --- a/go.mod +++ b/go.mod @@ -102,7 +102,6 @@ require ( github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3 github.com/coder/websocket v1.8.14 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc - github.com/exaring/otelpgx v0.10.0 github.com/getsentry/sentry-go v0.43.0 github.com/go-jose/go-jose/v4 v4.1.3 github.com/gocarina/gocsv v0.0.0-20240520201108-78e41c74b4b1 @@ -141,6 +140,7 @@ require ( github.com/onsi/gomega v1.39.1 github.com/openai/openai-go/v3 v3.24.0 github.com/openrdap/rdap v0.9.2-0.20240517203139-eb57b3a8dedd + github.com/overmindtech/otelpgx v0.10.1-0.20260303210427-65bf1016045e 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 diff --git a/go.sum b/go.sum index c46a7b11..33e323a3 100644 --- a/go.sum +++ b/go.sum @@ -391,8 +391,6 @@ 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/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= @@ -447,8 +445,6 @@ github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8 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= @@ -878,6 +874,8 @@ github.com/openai/openai-go/v3 v3.24.0 h1:08x6GnYiB+AAejTo6yzPY8RkZMJQ8NpreiOyM5 github.com/openai/openai-go/v3 v3.24.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/otelpgx v0.10.1-0.20260303210427-65bf1016045e h1:vP/Zs8Nbd902stVf7hBOd3VP/lIECgAjWR8pNBwcOu4= +github.com/overmindtech/otelpgx v0.10.1-0.20260303210427-65bf1016045e/go.mod h1:GtSjAg9Irz03mc8tPIh9/bKOx63sqyO752SABZhBdj0= 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= @@ -959,8 +957,6 @@ 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/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= diff --git a/go/tracing/main.go b/go/tracing/main.go index 013b65e3..6bb88933 100644 --- a/go/tracing/main.go +++ b/go/tracing/main.go @@ -298,10 +298,6 @@ func NewOvermindSampler() *OvermindSampler { SampleRate: 200, ShouldSample: UserAgentMatcher("ELB-HealthChecker/2.0", "kube-probe/1.27+"), }, - { - SampleRate: 10, - ShouldSample: SpanNameMatcher("pool.acquire"), - }, } // Pre-allocate samplers for each rule @@ -338,13 +334,6 @@ func UserAgentMatcher(userAgents ...string) func(sdktrace.SamplingParameters) bo } } -// 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 { diff --git a/k8s-source/adapters/endpoints.go b/k8s-source/adapters/endpoints.go index 0fb050eb..3f132ffe 100644 --- a/k8s-source/adapters/endpoints.go +++ b/k8s-source/adapters/endpoints.go @@ -1,3 +1,4 @@ +//nolint:staticcheck // TODO: migrate from v1.Endpoints to discoveryv1.EndpointSlice package adapters import ( From 86423c3e2d27907cb019c8ef77e3be9df23f3c84 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Wed, 4 Mar 2026 11:30:54 +0100 Subject: [PATCH 52/74] [ENG-2893] Migrate charmbracelet/lipgloss/v2 to charm.land/lipgloss/v2 (#4107) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image CLI Rendering as per normal ## Summary - Migrate from `github.com/charmbracelet/lipgloss/v2` (v2.0.0-beta.3) to `charm.land/lipgloss/v2` (v2.0.0) after the upstream module path change - Update imports in `cli/cmd/theme.go` and `cli/cmd/terraform_plan.go` - Bump `charmbracelet/x/cellbuf` v0.0.13 → v0.0.15 for transitive dependency compatibility ## Linear Ticket - **Ticket**: [ENG-2893](https://linear.app/overmind/issue/ENG-2893) — Migrate charmbracelet/lipgloss/v2 to charm.land/lipgloss/v2 module path - **Purpose**: The v2.0.0 release changed the Go module path, breaking `go mod download` when pinned to the old path ## Changes - `go.mod` / `go.sum`: Swapped module path and version, bumped cellbuf for compatibility - `cli/cmd/theme.go`: Updated lipgloss import path - `cli/cmd/terraform_plan.go`: Updated lipgloss import path - No API changes required — all lipgloss functions (`HasDarkBackground`, `LightDark`, `Color`, `NewStyle`) are unchanged between beta.3 and v2.0.0 --- > [!NOTE] > **Low Risk** > Low risk: changes are limited to dependency/module-path updates and import rewrites for terminal styling, with no business logic modifications. Main risk is build/runtime regressions from upgraded transitive Charm dependencies affecting CLI rendering. > > **Overview** > **Migrates Lip Gloss v2 to its new Go module path.** Updates CLI imports from `github.com/charmbracelet/lipgloss/v2` to `charm.land/lipgloss/v2`. > > **Bumps dependencies to match the new module.** `go.mod`/`go.sum` move from `lipgloss/v2` beta to `v2.0.0` and update related transitive Charm packages (e.g., `colorprofile`, `x/ansi`, `x/cellbuf`, `x/term`) plus a few indirect terminal-width/text deps. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5bfaf8e33fb04c045864ca433e6d54d21bc7fe8f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: fe4c947e211193e77c8cb0a9be1e4c6f8a96eaa1 --- cmd/terraform_plan.go | 2 +- cmd/theme.go | 2 +- go.mod | 22 +++++++++++-------- go.sum | 50 ++++++++++++++++++++++++++----------------- 4 files changed, 45 insertions(+), 31 deletions(-) diff --git a/cmd/terraform_plan.go b/cmd/terraform_plan.go index 2218fe33..0bf762af 100644 --- a/cmd/terraform_plan.go +++ b/cmd/terraform_plan.go @@ -12,7 +12,7 @@ import ( "time" "connectrpc.com/connect" - lipgloss "github.com/charmbracelet/lipgloss/v2" + lipgloss "charm.land/lipgloss/v2" "github.com/google/uuid" "github.com/muesli/reflow/wordwrap" "github.com/overmindtech/pterm" diff --git a/cmd/theme.go b/cmd/theme.go index e5c71cfa..bdb915eb 100644 --- a/cmd/theme.go +++ b/cmd/theme.go @@ -8,7 +8,7 @@ import ( "github.com/charmbracelet/glamour" "github.com/charmbracelet/glamour/ansi" - lipgloss "github.com/charmbracelet/lipgloss/v2" + lipgloss "charm.land/lipgloss/v2" ) // constrain the maximum terminal width to avoid readability issues with too diff --git a/go.mod b/go.mod index ac79e5e9..33b47b10 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( atomicgo.dev/keyboard v0.2.9 buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 buf.build/go/protovalidate v1.1.3 + charm.land/lipgloss/v2 v2.0.0 cloud.google.com/go/aiplatform v1.118.0 cloud.google.com/go/auth v0.18.2 cloud.google.com/go/bigquery v1.74.0 @@ -99,7 +100,6 @@ require ( 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/getsentry/sentry-go v0.43.0 @@ -258,14 +258,19 @@ require ( 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/colorprofile v0.4.2 // 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 - github.com/charmbracelet/x/ansi v0.8.0 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 // indirect + github.com/charmbracelet/x/ansi v0.11.6 // indirect + github.com/charmbracelet/x/cellbuf v0.0.15 // indirect github.com/charmbracelet/x/exp/slice v0.0.0-20250417172821-98fd948af1b1 // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/charmbracelet/x/term v0.2.2 // indirect + github.com/charmbracelet/x/termios v0.1.1 // indirect + github.com/charmbracelet/x/windows v0.2.2 // indirect github.com/cheggaaa/pb/v3 v3.1.4 // indirect github.com/chzyer/readline v1.5.1 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cloudflare/circl v1.6.3 // indirect github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/cnf/structhash v0.0.0-20201127153200-e1b16c1ebc08 // indirect @@ -289,6 +294,7 @@ require ( 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-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // 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 @@ -365,12 +371,12 @@ require ( 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/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/lucasb-eyer/go-colorful v1.3.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.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mattn/go-runewidth v0.0.19 // 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 @@ -514,5 +520,3 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.2 sigs.k8s.io/yaml v1.6.0 // indirect ) - -require github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect diff --git a/go.sum b/go.sum index 33e323a3..7673863e 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ buf.build/go/protovalidate v1.1.3 h1:m2GVEgQWd7rk+vIoAZ+f0ygGjvQTuqPQapBBdcpWVPE buf.build/go/protovalidate v1.1.3/go.mod h1:9XIuohWz+kj+9JVn3WQneHA5LZP50mjvneZMnbLkiIE= cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo= +charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14= 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= @@ -318,8 +320,8 @@ github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5r 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/aymanbagabas/go-udiff v0.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM= +github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= 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= @@ -328,8 +330,8 @@ github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLj 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/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= +github.com/bits-and-blooms/bitset v1.24.4/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= @@ -360,24 +362,28 @@ github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F9 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= -github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= +github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= +github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8= github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= -github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3 h1:W6DpZX6zSkZr0iFq6JVh1vItLoxfYtNlaxOJtWp8Kis= -github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.3/go.mod h1:65HTtKURcv/ict9ZQhr6zT84JqIjMcJbyrZYHHKNfKA= -github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= -github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= -github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= -github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= -github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 h1:OqDqxQZliC7C8adA7KjelW3OjtAxREfeHkNcd66wpeI= +github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318/go.mod h1:Y6kE2GzHfkyQQVCSL9r2hwokSrIlHGzZG+71+wDYSZI= +github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8= +github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= +github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= +github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= +github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/exp/slice v0.0.0-20250417172821-98fd948af1b1 h1:8fUBSeb8wmOWD0ToP8AJFhUCYrmR3aj/sLECrLGM0TI= 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/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= +github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= +github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= 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= @@ -390,6 +396,10 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn 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/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= @@ -770,8 +780,8 @@ github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8 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/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= +github.com/lucasb-eyer/go-colorful v1.3.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= @@ -790,8 +800,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE 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/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= 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= From 00e79e75284bd65fab07d19666297508a77573b5 Mon Sep 17 00:00:00 2001 From: carabasdaniel Date: Wed, 4 Mar 2026 12:40:56 +0200 Subject: [PATCH 53/74] feat(cli): exit non-zero from knowledge list when invalid files (#4109) - Return ErrInvalidKnowledgeFiles from renderKnowledgeList when any invalid/skipped knowledge files exist so the command can be used as a CI gate. - Always print the full listing (valid + invalid sections) before returning the error so CI logs still show what failed and why. - Tests updated to expect the error and to assert errors.Is(..., ErrInvalidKnowledgeFiles). --- > [!NOTE] > **Medium Risk** > Changes CLI exit behavior by turning discovery warnings into a command error, which may break existing scripts/CI expectations. Scope is limited to `knowledge list` output/error handling and corresponding tests. > > **Overview** > `knowledge list` now treats any invalid/skipped knowledge files as a failure by returning `ErrInvalidKnowledgeFiles` (with a count) when discovery emits warnings. > > The command was adjusted to always print the rendered listing before returning the error so CI logs still include both the valid table and the invalid/skipped reasons; tests were updated to assert `errors.Is(err, ErrInvalidKnowledgeFiles)` when invalid files are present. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e96569773a500d1f4cde2c916d2e8b55be936b11. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: cc79081493018979fca6aeef132dbe9039346772 --- cmd/knowledge_list.go | 9 +++++++-- cmd/knowledge_list_test.go | 15 +++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cmd/knowledge_list.go b/cmd/knowledge_list.go index 6ab4c8f1..44bdfc47 100644 --- a/cmd/knowledge_list.go +++ b/cmd/knowledge_list.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "strings" @@ -10,6 +11,10 @@ import ( "github.com/spf13/viper" ) +// ErrInvalidKnowledgeFiles is returned when one or more knowledge files are invalid/skipped. +// Used so "knowledge list" can exit non-zero in CI when invalid files are found. +var ErrInvalidKnowledgeFiles = errors.New("invalid knowledge files found") + // knowledgeListCmd represents the knowledge list command var knowledgeListCmd = &cobra.Command{ Use: "list", @@ -21,11 +26,10 @@ var knowledgeListCmd = &cobra.Command{ func KnowledgeList(cmd *cobra.Command, args []string) error { startDir := viper.GetString("dir") output, err := renderKnowledgeList(startDir) + fmt.Print(output) if err != nil { return err } - - fmt.Print(output) return nil } @@ -85,6 +89,7 @@ func renderKnowledgeList(startDir string) (string, error) { fmt.Fprintf(&output, " Reason: %s\n", w.Reason) } output.WriteString("\n") + return output.String(), fmt.Errorf("%w (%d file(s))", ErrInvalidKnowledgeFiles, len(warnings)) } return output.String(), nil diff --git a/cmd/knowledge_list_test.go b/cmd/knowledge_list_test.go index 26b4dae4..35c12a0d 100644 --- a/cmd/knowledge_list_test.go +++ b/cmd/knowledge_list_test.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "os" "path/filepath" "strings" @@ -143,8 +144,11 @@ This file is missing frontmatter. `) output, err := renderKnowledgeList(dir) - if err != nil { - t.Fatalf("unexpected error: %v", err) + if err == nil { + t.Fatal("expected error when invalid files present, got nil") + } + if !errors.Is(err, ErrInvalidKnowledgeFiles) { + t.Errorf("expected ErrInvalidKnowledgeFiles, got: %v", err) } // Check for valid file @@ -185,8 +189,11 @@ Content. `) output, err := renderKnowledgeList(dir) - if err != nil { - t.Fatalf("unexpected error: %v", err) + if err == nil { + t.Fatal("expected error when only invalid files present, got nil") + } + if !errors.Is(err, ErrInvalidKnowledgeFiles) { + t.Errorf("expected ErrInvalidKnowledgeFiles, got: %v", err) } // Should NOT have valid files section From 74704276c41b20af0db0b054c1ae828cec7ec35c Mon Sep 17 00:00:00 2001 From: carabasdaniel Date: Wed, 4 Mar 2026 16:55:09 +0200 Subject: [PATCH 54/74] Area51 snapshot JSON format (#4075) Implement JSON snapshot support for Area51 and the snapshot source to improve consistency, ease of inspection, diffing, and standard tooling for the benchmarking and snapshot pipeline, and enable downloading revlink warmup snapshots as JSON files. --- Linear Issue: [ENG-2901](https://linear.app/overmind/issue/ENG-2901/json-snapshots-for-area51-and-snapshot-source)

Open in Web Open in Cursor 

--------- Co-authored-by: Cursor Agent Co-authored-by: carabasdaniel Co-authored-by: David Schmitt GitOrigin-RevId: 90cb509888ed50c26a2df173d1c5d962b88d0e57 --- sources/snapshot/README.md | 15 +++++++----- sources/snapshot/adapters/loader.go | 30 +++++++++++++++++------- sources/snapshot/adapters/loader_test.go | 16 +++++++++---- 3 files changed, 42 insertions(+), 19 deletions(-) diff --git a/sources/snapshot/README.md b/sources/snapshot/README.md index 4981e1d7..cf949ef7 100644 --- a/sources/snapshot/README.md +++ b/sources/snapshot/README.md @@ -4,7 +4,7 @@ A discovery source that serves items from a snapshot file or URL, enabling local ## 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: +The snapshot source loads a snapshot file (JSON or protobuf format) 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 @@ -12,7 +12,8 @@ The snapshot source loads a protobuf snapshot (`.pb` file) at startup and respon ## Features -- **Snapshot loading**: Loads snapshots from local files or HTTP(S) URLs +- **Snapshot loading**: Loads snapshots from local files or HTTP(S) URLs (JSON or protobuf format) +- **Format detection**: Automatically detects JSON (`.json`) or protobuf (`.pb`) format - **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 @@ -45,7 +46,7 @@ The snapshot source requires a snapshot file or URL to be specified: ```bash ALLOW_UNAUTHENTICATED=true \ -SNAPSHOT_SOURCE=/workspace/services/api-server/service/changeanalysis/testdata/snapshot.pb \ +SNAPSHOT_SOURCE=/workspace/services/api-server/service/changeanalysis/testdata/snapshot.json \ NATS_SERVICE_HOST=nats \ NATS_SERVICE_PORT=4222 \ go run ./sources/snapshot/main.go --log=debug --json-log=false @@ -64,7 +65,7 @@ Update the `SNAPSHOT_SOURCE` environment variable in the launch config to point ```bash ALLOW_UNAUTHENTICATED=true \ -SNAPSHOT_SOURCE=https://gateway-host/area51/snapshots/{uuid}/protobuf \ +SNAPSHOT_SOURCE=https://gateway-host/area51/snapshots/{uuid}/json \ NATS_SERVICE_HOST=nats \ NATS_SERVICE_PORT=4222 \ go run ./sources/snapshot/main.go @@ -130,7 +131,7 @@ go test -v Test snapshot loading: ```bash cd sources/snapshot -go run main.go --snapshot-source=/path/to/snapshot.pb --help +go run main.go --snapshot-source=/path/to/snapshot.json --help ``` Verify with real snapshot: @@ -166,4 +167,6 @@ go test -run TestLoadSnapshotFromFile -v ./adapters - **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` +- **Test snapshots**: + - JSON format (recommended): `services/api-server/service/changeanalysis/testdata/snapshot.json` + - Protobuf format (legacy): `services/api-server/service/changeanalysis/testdata/snapshot.pb` diff --git a/sources/snapshot/adapters/loader.go b/sources/snapshot/adapters/loader.go index 8f702285..eb1c9133 100644 --- a/sources/snapshot/adapters/loader.go +++ b/sources/snapshot/adapters/loader.go @@ -1,6 +1,7 @@ package adapters import ( + "bytes" "context" "fmt" "io" @@ -10,6 +11,7 @@ import ( "github.com/overmindtech/cli/go/sdp-go" log "github.com/sirupsen/logrus" + "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" ) @@ -33,20 +35,32 @@ func LoadSnapshot(ctx context.Context, source string) (*sdp.Snapshot, error) { } } - // Unmarshal the protobuf data + // Unmarshal the data (detect JSON vs protobuf format) snapshot := &sdp.Snapshot{} - if err := proto.Unmarshal(data, snapshot); err != nil { - return nil, fmt.Errorf("failed to unmarshal snapshot protobuf: %w", err) + trimmed := bytes.TrimSpace(data) + if len(trimmed) > 0 && trimmed[0] == '{' { + // JSON format + if err := protojson.Unmarshal(data, snapshot); err != nil { + return nil, fmt.Errorf("failed to unmarshal snapshot JSON: %w", err) + } + log.Info("Loaded snapshot from JSON format") + } else { + // Protobuf format + if err := proto.Unmarshal(data, snapshot); err != nil { + return nil, fmt.Errorf("failed to unmarshal snapshot protobuf: %w", err) + } + log.Info("Loaded snapshot from protobuf format") } - // Validate snapshot has items - if snapshot.GetProperties() == nil || len(snapshot.GetProperties().GetItems()) == 0 { - return nil, fmt.Errorf("snapshot has no items") + if snapshot.GetProperties() == nil { + return nil, fmt.Errorf("snapshot has no properties") } + items := len(snapshot.GetProperties().GetItems()) + edges := len(snapshot.GetProperties().GetEdges()) log.WithFields(log.Fields{ - "items": len(snapshot.GetProperties().GetItems()), - "edges": len(snapshot.GetProperties().GetEdges()), + "items": items, + "edges": edges, }).Info("Snapshot loaded successfully") return snapshot, nil diff --git a/sources/snapshot/adapters/loader_test.go b/sources/snapshot/adapters/loader_test.go index ff8c5053..a2b65c40 100644 --- a/sources/snapshot/adapters/loader_test.go +++ b/sources/snapshot/adapters/loader_test.go @@ -107,7 +107,7 @@ func TestLoadSnapshotFromURL(t *testing.T) { } func TestLoadSnapshotEmptyItems(t *testing.T) { - // Create a snapshot with no items + // Create a snapshot with no items (e.g. revlink warmup for account with no sources) snapshot := &sdp.Snapshot{ Properties: &sdp.SnapshotProperties{ Name: "empty-snapshot", @@ -128,11 +128,17 @@ func TestLoadSnapshotEmptyItems(t *testing.T) { t.Fatalf("Failed to write test snapshot file: %v", err) } - // Test loading - should fail validation + // Empty snapshots are allowed (e.g. for benchmarking or accounts with no discovered infra) ctx := context.Background() - _, err = LoadSnapshot(ctx, tmpFile) - if err == nil { - t.Error("Expected error for snapshot with no items, got nil") + loaded, err := LoadSnapshot(ctx, tmpFile) + if err != nil { + t.Fatalf("LoadSnapshot with empty items should succeed: %v", err) + } + if len(loaded.GetProperties().GetItems()) != 0 { + t.Errorf("Expected 0 items, got %d", len(loaded.GetProperties().GetItems())) + } + if loaded.GetProperties().GetName() != "empty-snapshot" { + t.Errorf("Expected name 'empty-snapshot', got %q", loaded.GetProperties().GetName()) } } From dd2668cb8cc1d61594f237d1c1687fca23359bca Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:48:30 +0000 Subject: [PATCH 55/74] Add Azure DNS Record Sets Client and Adapter Implementation (#4108) image --- > [!NOTE] > **Medium Risk** > Introduces a new Azure DNS record set adapter and changes shared Azure terraform-query parsing in `transformer.go`, which could affect how existing Azure resource-ID searches are interpreted and error out. > > **Overview** > Adds first-class discovery for Azure DNS record sets via a new `RecordSetsClient` wrapper and a `NewNetworkDNSRecordSet` manual adapter (Get/Search/SearchStream), including link generation to the parent DNS zone plus related `stdlib` DNS/IP items. > > Extends Azure resource-ID parsing to support the non-standard DNS record set path format and updates `transformer.go` to use `ExtractPathParamsFromResourceIDByType`, returning clearer errors when the adapter type is unknown vs the ID format is invalid. The new adapter is registered in `manual/adapters.go` and covered by unit tests plus a generated GoMock client. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7f18743a8e898139f65e5a9578e316a6500bec0c. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 4646b48a153dec066177e249f731d836d01dea5d --- sources/azure/clients/record-sets-client.go | 35 ++ sources/azure/manual/adapters.go | 9 + .../azure/manual/network-dns-record-set.go | 374 ++++++++++++++++++ .../manual/network-dns-record-set_test.go | 313 +++++++++++++++ .../shared/mocks/mock_record_sets_client.go | 72 ++++ sources/azure/shared/utils.go | 37 ++ sources/azure/shared/utils_test.go | 67 ++++ sources/transformer.go | 39 +- 8 files changed, 936 insertions(+), 10 deletions(-) create mode 100644 sources/azure/clients/record-sets-client.go create mode 100644 sources/azure/manual/network-dns-record-set.go create mode 100644 sources/azure/manual/network-dns-record-set_test.go create mode 100644 sources/azure/shared/mocks/mock_record_sets_client.go diff --git a/sources/azure/clients/record-sets-client.go b/sources/azure/clients/record-sets-client.go new file mode 100644 index 00000000..e54b996c --- /dev/null +++ b/sources/azure/clients/record-sets-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" +) + +//go:generate mockgen -destination=../shared/mocks/mock_record_sets_client.go -package=mocks -source=record-sets-client.go + +// RecordSetsPager is a type alias for the generic Pager interface with record sets list response type. +type RecordSetsPager = Pager[armdns.RecordSetsClientListAllByDNSZoneResponse] + +// RecordSetsClient is an interface for interacting with Azure DNS record sets +type RecordSetsClient interface { + Get(ctx context.Context, resourceGroupName string, zoneName string, relativeRecordSetName string, recordType armdns.RecordType, options *armdns.RecordSetsClientGetOptions) (armdns.RecordSetsClientGetResponse, error) + NewListAllByDNSZonePager(resourceGroupName string, zoneName string, options *armdns.RecordSetsClientListAllByDNSZoneOptions) RecordSetsPager +} + +type recordSetsClient struct { + client *armdns.RecordSetsClient +} + +func (c *recordSetsClient) Get(ctx context.Context, resourceGroupName string, zoneName string, relativeRecordSetName string, recordType armdns.RecordType, options *armdns.RecordSetsClientGetOptions) (armdns.RecordSetsClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, zoneName, relativeRecordSetName, recordType, options) +} + +func (c *recordSetsClient) NewListAllByDNSZonePager(resourceGroupName string, zoneName string, options *armdns.RecordSetsClientListAllByDNSZoneOptions) RecordSetsPager { + return c.client.NewListAllByDNSZonePager(resourceGroupName, zoneName, options) +} + +// NewRecordSetsClient creates a new RecordSetsClient from the Azure SDK client +func NewRecordSetsClient(client *armdns.RecordSetsClient) RecordSetsClient { + return &recordSetsClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 9f80db28..395549f0 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -328,6 +328,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) } + recordSetsClient, err := armdns.NewRecordSetsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create record sets 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) @@ -494,6 +498,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewZonesClient(zonesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewNetworkDNSRecordSet( + clients.NewRecordSetsClient(recordSetsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewBatchAccount( clients.NewBatchAccountsClient(batchAccountsClient), resourceGroupScopes, @@ -678,6 +686,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewNetworkPublicIPAddress(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkLoadBalancer(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkZone(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewNetworkDNSRecordSet(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewBatchAccount(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewBatchBatchApplication(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewBatchBatchPool(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/network-dns-record-set.go b/sources/azure/manual/network-dns-record-set.go new file mode 100644 index 00000000..c2079691 --- /dev/null +++ b/sources/azure/manual/network-dns-record-set.go @@ -0,0 +1,374 @@ +package manual + +import ( + "context" + "errors" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" + "github.com/overmindtech/cli/go/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 NetworkDNSRecordSetLookupByRecordType = shared.NewItemTypeLookup("recordType", azureshared.NetworkDNSRecordSet) +var NetworkDNSRecordSetLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkDNSRecordSet) + +type networkDNSRecordSetWrapper struct { + client clients.RecordSetsClient + + *azureshared.MultiResourceGroupBase +} + +func NewNetworkDNSRecordSet(client clients.RecordSetsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.SearchableWrapper { + return &networkDNSRecordSetWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + azureshared.NetworkDNSRecordSet, + ), + } +} + +// recordTypeFromResourceType extracts the DNS record type (e.g. "A", "AAAA") from the ARM resource type (e.g. "Microsoft.Network/dnszones/A"). +func recordTypeFromResourceType(resourceType string) string { + if resourceType == "" { + return "" + } + parts := strings.Split(resourceType, "/") + if len(parts) > 0 { + return parts[len(parts)-1] + } + return "" +} + +//ref: https://learn.microsoft.com/en-us/rest/api/dns/record-sets/get?view=rest-dns-2018-05-01&tabs=HTTP +func (n networkDNSRecordSetWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 3 { + return nil, azureshared.QueryError(errors.New("Get requires 3 query parts: zoneName, recordType, and relativeRecordSetName"), scope, n.Type()) + } + zoneName := queryParts[0] + recordTypeStr := queryParts[1] + relativeRecordSetName := queryParts[2] + if zoneName == "" || recordTypeStr == "" || relativeRecordSetName == "" { + return nil, azureshared.QueryError(errors.New("zoneName, recordType and relativeRecordSetName cannot be empty"), scope, n.Type()) + } + recordType := armdns.RecordType(recordTypeStr) + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + resp, err := n.client.Get(ctx, rgScope.ResourceGroup, zoneName, relativeRecordSetName, recordType, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + return n.azureRecordSetToSDPItem(&resp.RecordSet, zoneName, scope) +} + +func (n networkDNSRecordSetWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + NetworkZoneLookupByName, + NetworkDNSRecordSetLookupByRecordType, + NetworkDNSRecordSetLookupByName, + } +} + +func (n networkDNSRecordSetWrapper) 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: zoneName"), scope, n.Type()) + } + zoneName := queryParts[0] + if zoneName == "" { + return nil, azureshared.QueryError(errors.New("zoneName cannot be empty"), scope, n.Type()) + } + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + pager := n.client.NewListAllByDNSZonePager(rgScope.ResourceGroup, zoneName, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + for _, rs := range page.Value { + if rs == nil || rs.Name == nil { + continue + } + item, sdpErr := n.azureRecordSetToSDPItem(rs, zoneName, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (n networkDNSRecordSetWrapper) 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: zoneName"), scope, n.Type())) + return + } + zoneName := queryParts[0] + if zoneName == "" { + stream.SendError(azureshared.QueryError(errors.New("zoneName cannot be empty"), scope, n.Type())) + return + } + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + pager := n.client.NewListAllByDNSZonePager(rgScope.ResourceGroup, zoneName, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + for _, rs := range page.Value { + if rs == nil || rs.Name == nil { + continue + } + item, sdpErr := n.azureRecordSetToSDPItem(rs, zoneName, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (n networkDNSRecordSetWrapper) SearchLookups() []sources.ItemTypeLookups { + return []sources.ItemTypeLookups{ + {NetworkZoneLookupByName}, + } +} + +func (n networkDNSRecordSetWrapper) azureRecordSetToSDPItem(rs *armdns.RecordSet, zoneName, scope string) (*sdp.Item, *sdp.QueryError) { + if rs.Name == nil { + return nil, azureshared.QueryError(errors.New("record set name is nil"), scope, n.Type()) + } + relativeName := *rs.Name + recordTypeStr := "" + if rs.Type != nil { + recordTypeStr = recordTypeFromResourceType(*rs.Type) + } + if recordTypeStr == "" { + return nil, azureshared.QueryError(errors.New("record set type is nil or invalid"), scope, n.Type()) + } + + attributes, err := shared.ToAttributesWithExclude(rs, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + uniqueAttr := shared.CompositeLookupKey(zoneName, recordTypeStr, relativeName) + if err := attributes.Set("uniqueAttr", uniqueAttr); err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.NetworkDNSRecordSet.String(), + UniqueAttribute: "uniqueAttr", + Attributes: attributes, + Scope: scope, + } + + // Link to parent DNS zone + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkZone.String(), + Method: sdp.QueryMethod_GET, + Query: zoneName, + Scope: scope, + }, + }) + + // Link to DNS name (standard library) from FQDN if present + if rs.Properties != nil && rs.Properties.Fqdn != nil && *rs.Properties.Fqdn != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: *rs.Properties.Fqdn, + Scope: "global", + }, + }) + } + + // LinkedItemQueries for IP addresses and DNS names in record data + if rs.Properties != nil { + seenIPs := make(map[string]struct{}) + seenDNS := make(map[string]struct{}) + + // A records (IPv4) -> stdlib.NetworkIP, GET, global + for _, a := range rs.Properties.ARecords { + if a != nil && a.IPv4Address != nil && *a.IPv4Address != "" { + ip := *a.IPv4Address + if _, seen := seenIPs[ip]; !seen { + seenIPs[ip] = struct{}{} + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: ip, + Scope: "global", + }, + }) + } + } + } + // AAAA records (IPv6) -> stdlib.NetworkIP, GET, global + for _, aaaa := range rs.Properties.AaaaRecords { + if aaaa != nil && aaaa.IPv6Address != nil && *aaaa.IPv6Address != "" { + ip := *aaaa.IPv6Address + if _, seen := seenIPs[ip]; !seen { + seenIPs[ip] = struct{}{} + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: ip, + Scope: "global", + }, + }) + } + } + } + + // DNS names in record data -> stdlib.NetworkDNS, SEARCH, global + appendDNSLink := func(name string) { + if name == "" { + return + } + if _, seen := seenDNS[name]; !seen { + seenDNS[name] = struct{}{} + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: name, + Scope: "global", + }, + }) + } + } + if rs.Properties.CnameRecord != nil && rs.Properties.CnameRecord.Cname != nil && *rs.Properties.CnameRecord.Cname != "" { + appendDNSLink(*rs.Properties.CnameRecord.Cname) + } + for _, mx := range rs.Properties.MxRecords { + if mx != nil && mx.Exchange != nil && *mx.Exchange != "" { + appendDNSLink(*mx.Exchange) + } + } + for _, ns := range rs.Properties.NsRecords { + if ns != nil && ns.Nsdname != nil && *ns.Nsdname != "" { + appendDNSLink(*ns.Nsdname) + } + } + for _, ptr := range rs.Properties.PtrRecords { + if ptr != nil && ptr.Ptrdname != nil && *ptr.Ptrdname != "" { + appendDNSLink(*ptr.Ptrdname) + } + } + // SOA Host is the authoritative name server (DNS name). SOA Email is an email in DNS + // notation (e.g. admin.example.com = admin@example.com), not a resolvable hostname. + if rs.Properties.SoaRecord != nil && rs.Properties.SoaRecord.Host != nil && *rs.Properties.SoaRecord.Host != "" { + appendDNSLink(*rs.Properties.SoaRecord.Host) + } + // Only "issue" and "issuewild" CAA values are DNS names (CA domain). "iodef" values + // are URLs (e.g. mailto: or https:) and must not be passed to appendDNSLink. + for _, caa := range rs.Properties.CaaRecords { + if caa == nil || caa.Tag == nil || caa.Value == nil || *caa.Value == "" { + continue + } + tag := *caa.Tag + if tag != "issue" && tag != "issuewild" { + continue + } + appendDNSLink(*caa.Value) + } + for _, srv := range rs.Properties.SrvRecords { + if srv != nil && srv.Target != nil && *srv.Target != "" { + appendDNSLink(*srv.Target) + } + } + + // TargetResource (Azure resource ID) -> link to referenced resource. + // Pass the composite lookup key (extracted query parts) so the target adapter's Get + // receives the expected parts when the transformer splits by QuerySeparator; it does + // not parse full resource IDs for linked GET queries. + if rs.Properties.TargetResource != nil && rs.Properties.TargetResource.ID != nil && *rs.Properties.TargetResource.ID != "" { + targetID := *rs.Properties.TargetResource.ID + linkScope := azureshared.ExtractScopeFromResourceID(targetID) + if linkScope == "" { + linkScope = scope + } + itemType := azureshared.ItemTypeFromLinkedResourceID(targetID) + queryParts := azureshared.ExtractPathParamsFromResourceIDByType(itemType, targetID) + if itemType != "" && queryParts != nil { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: itemType, + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(queryParts...), + Scope: linkScope, + }, + }) + } + } + } + + // Health from provisioning state + if rs.Properties != nil && rs.Properties.ProvisioningState != nil { + switch *rs.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() + } + } + + return sdpItem, nil +} + +func (n networkDNSRecordSetWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.NetworkZone, + stdlib.NetworkDNS, + stdlib.NetworkIP, + ) +} + +func (n networkDNSRecordSetWrapper) TerraformMappings() []*sdp.TerraformMapping { + return nil +} + +func (n networkDNSRecordSetWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Network/dnszones/*/read", + } +} + +func (n networkDNSRecordSetWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/network-dns-record-set_test.go b/sources/azure/manual/network-dns-record-set_test.go new file mode 100644 index 00000000..4ab57c26 --- /dev/null +++ b/sources/azure/manual/network-dns-record-set_test.go @@ -0,0 +1,313 @@ +package manual_test + +import ( + "context" + "errors" + "slices" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" + "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/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 createAzureRecordSet(relativeName, recordType, zoneName, subscriptionID, resourceGroup string) *armdns.RecordSet { + fqdn := relativeName + "." + zoneName + armType := "Microsoft.Network/dnszones/" + recordType + provisioningState := "Succeeded" + return &armdns.RecordSet{ + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/dnszones/" + zoneName + "/" + recordType + "/" + relativeName), + Name: new(relativeName), + Type: new(armType), + Properties: &armdns.RecordSetProperties{ + Fqdn: new(fqdn), + ProvisioningState: &provisioningState, + TTL: new(int64(3600)), + ARecords: nil, + AaaaRecords: nil, + CnameRecord: nil, + MxRecords: nil, + NsRecords: nil, + PtrRecords: nil, + SoaRecord: nil, + SrvRecords: nil, + TxtRecords: nil, + CaaRecords: nil, + TargetResource: nil, + Metadata: nil, + }, + } +} + +type mockRecordSetsPager struct { + pages []armdns.RecordSetsClientListAllByDNSZoneResponse + index int +} + +func (m *mockRecordSetsPager) More() bool { + return m.index < len(m.pages) +} + +func (m *mockRecordSetsPager) NextPage(ctx context.Context) (armdns.RecordSetsClientListAllByDNSZoneResponse, error) { + if m.index >= len(m.pages) { + return armdns.RecordSetsClientListAllByDNSZoneResponse{}, errors.New("no more pages") + } + page := m.pages[m.index] + m.index++ + return page, nil +} + +func TestNetworkDNSRecordSet(t *testing.T) { + ctx := context.Background() + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionID := "test-subscription" + resourceGroup := "test-rg" + zoneName := "example.com" + relativeName := "www" + recordType := "A" + query := shared.CompositeLookupKey(zoneName, recordType, relativeName) + + t.Run("Get", func(t *testing.T) { + rs := createAzureRecordSet(relativeName, recordType, zoneName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockRecordSetsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, zoneName, relativeName, armdns.RecordType(recordType), nil).Return( + armdns.RecordSetsClientGetResponse{ + RecordSet: *rs, + }, nil) + + wrapper := manual.NewNetworkDNSRecordSet(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + 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.NetworkDNSRecordSet.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkDNSRecordSet.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "uniqueAttr" { + t.Errorf("Expected unique attribute 'uniqueAttr', got %s", sdpItem.GetUniqueAttribute()) + } + + expectedUnique := shared.CompositeLookupKey(zoneName, recordType, relativeName) + 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.NetworkZone.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: zoneName, + ExpectedScope: subscriptionID + "." + resourceGroup, + }, + { + ExpectedType: stdlib.NetworkDNS.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "www.example.com", + ExpectedScope: "global", + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockRecordSetsClient(ctrl) + wrapper := manual.NewNetworkDNSRecordSet(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + // Single part (zone only) is insufficient + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], zoneName, true) + if qErr == nil { + t.Error("Expected error when providing only one query part, got nil") + } + }) + + t.Run("Search", func(t *testing.T) { + rs1 := createAzureRecordSet("www", "A", zoneName, subscriptionID, resourceGroup) + rs2 := createAzureRecordSet("mail", "MX", zoneName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockRecordSetsClient(ctrl) + mockPager := &mockRecordSetsPager{ + pages: []armdns.RecordSetsClientListAllByDNSZoneResponse{ + { + RecordSetListResult: armdns.RecordSetListResult{ + Value: []*armdns.RecordSet{rs1, rs2}, + }, + }, + }, + } + mockClient.EXPECT().NewListAllByDNSZonePager(resourceGroup, zoneName, nil).Return(mockPager) + + wrapper := manual.NewNetworkDNSRecordSet(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, qErr := searchable.Search(ctx, wrapper.Scopes()[0], zoneName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + if len(items) != 2 { + t.Fatalf("Expected 2 items, got %d", len(items)) + } + for _, item := range items { + if item.Validate() != nil { + t.Fatalf("Expected valid item, got: %v", item.Validate()) + } + } + }) + + t.Run("SearchStream", func(t *testing.T) { + rs := createAzureRecordSet("www", "A", zoneName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockRecordSetsClient(ctrl) + mockPager := &mockRecordSetsPager{ + pages: []armdns.RecordSetsClientListAllByDNSZoneResponse{ + { + RecordSetListResult: armdns.RecordSetListResult{ + Value: []*armdns.RecordSet{rs}, + }, + }, + }, + } + mockClient.EXPECT().NewListAllByDNSZonePager(resourceGroup, zoneName, nil).Return(mockPager) + + wrapper := manual.NewNetworkDNSRecordSet(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + streamable, ok := adapter.(discovery.SearchStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not implement SearchStreamableAdapter") + } + + var received []*sdp.Item + stream := discovery.NewQueryResultStream( + func(item *sdp.Item) { received = append(received, item) }, + func(error) {}, + ) + streamable.SearchStream(ctx, wrapper.Scopes()[0], zoneName, true, stream) + + if len(received) != 1 { + t.Fatalf("Expected 1 item from SearchStream, got %d", len(received)) + } + if received[0].GetType() != azureshared.NetworkDNSRecordSet.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkDNSRecordSet.String(), received[0].GetType()) + } + }) + + t.Run("SearchWithInsufficientQueryParts", func(t *testing.T) { + mockClient := mocks.NewMockRecordSetsClient(ctrl) + wrapper := manual.NewNetworkDNSRecordSet(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + searchable := adapter.(discovery.SearchableAdapter) + _, qErr := searchable.Search(ctx, wrapper.Scopes()[0], "", true) + if qErr == nil { + t.Error("Expected error when providing empty zone name, got nil") + } + }) + + t.Run("ErrorHandling", func(t *testing.T) { + expectedErr := errors.New("record set not found") + mockClient := mocks.NewMockRecordSetsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, zoneName, relativeName, armdns.RecordType(recordType), nil).Return( + armdns.RecordSetsClientGetResponse{}, expectedErr) + + wrapper := manual.NewNetworkDNSRecordSet(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr == nil { + t.Error("Expected error when Get fails, got nil") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + mockClient := mocks.NewMockRecordSetsClient(ctrl) + wrapper := manual.NewNetworkDNSRecordSet(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + potentialLinks := wrapper.PotentialLinks() + if !potentialLinks[azureshared.NetworkZone] { + t.Error("Expected PotentialLinks to include NetworkZone") + } + if !potentialLinks[stdlib.NetworkDNS] { + t.Error("Expected PotentialLinks to include stdlib.NetworkDNS") + } + if !potentialLinks[stdlib.NetworkIP] { + t.Error("Expected PotentialLinks to include stdlib.NetworkIP") + } + }) + + t.Run("IAMPermissions", func(t *testing.T) { + mockClient := mocks.NewMockRecordSetsClient(ctrl) + wrapper := manual.NewNetworkDNSRecordSet(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + perms := wrapper.IAMPermissions() + if len(perms) == 0 { + t.Error("Expected at least one IAM permission") + } + expectedPermission := "Microsoft.Network/dnszones/*/read" + found := slices.Contains(perms, expectedPermission) + if !found { + t.Errorf("Expected IAMPermissions to include %q", expectedPermission) + } + }) + + t.Run("GetWithARecordsAndCnameLinkedQueries", func(t *testing.T) { + rs := createAzureRecordSet(relativeName, recordType, zoneName, subscriptionID, resourceGroup) + rs.Properties.ARecords = []*armdns.ARecord{{IPv4Address: new("192.168.1.1")}} + rs.Properties.CnameRecord = &armdns.CnameRecord{Cname: new("backend.example.com")} + + mockClient := mocks.NewMockRecordSetsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, zoneName, relativeName, armdns.RecordType(recordType), nil).Return( + armdns.RecordSetsClientGetResponse{RecordSet: *rs}, nil) + + wrapper := manual.NewNetworkDNSRecordSet(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], query, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + var hasIPLink, hasCnameLink bool + for _, lq := range sdpItem.GetLinkedItemQueries() { + q := lq.GetQuery() + if q == nil { + continue + } + if q.GetType() == stdlib.NetworkIP.String() && q.GetQuery() == "192.168.1.1" && q.GetMethod() == sdp.QueryMethod_GET && q.GetScope() == "global" { + hasIPLink = true + } + if q.GetType() == stdlib.NetworkDNS.String() && q.GetQuery() == "backend.example.com" && q.GetMethod() == sdp.QueryMethod_SEARCH && q.GetScope() == "global" { + hasCnameLink = true + } + } + if !hasIPLink { + t.Error("Expected LinkedItemQueries to include stdlib.NetworkIP for A record 192.168.1.1 (GET, global)") + } + if !hasCnameLink { + t.Error("Expected LinkedItemQueries to include stdlib.NetworkDNS for CNAME backend.example.com (SEARCH, global)") + } + }) +} diff --git a/sources/azure/shared/mocks/mock_record_sets_client.go b/sources/azure/shared/mocks/mock_record_sets_client.go new file mode 100644 index 00000000..241c507b --- /dev/null +++ b/sources/azure/shared/mocks/mock_record_sets_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: record-sets-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_record_sets_client.go -package=mocks -source=record-sets-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armdns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockRecordSetsClient is a mock of RecordSetsClient interface. +type MockRecordSetsClient struct { + ctrl *gomock.Controller + recorder *MockRecordSetsClientMockRecorder + isgomock struct{} +} + +// MockRecordSetsClientMockRecorder is the mock recorder for MockRecordSetsClient. +type MockRecordSetsClientMockRecorder struct { + mock *MockRecordSetsClient +} + +// NewMockRecordSetsClient creates a new mock instance. +func NewMockRecordSetsClient(ctrl *gomock.Controller) *MockRecordSetsClient { + mock := &MockRecordSetsClient{ctrl: ctrl} + mock.recorder = &MockRecordSetsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRecordSetsClient) EXPECT() *MockRecordSetsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockRecordSetsClient) Get(ctx context.Context, resourceGroupName, zoneName, relativeRecordSetName string, recordType armdns.RecordType, options *armdns.RecordSetsClientGetOptions) (armdns.RecordSetsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, zoneName, relativeRecordSetName, recordType, options) + ret0, _ := ret[0].(armdns.RecordSetsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockRecordSetsClientMockRecorder) Get(ctx, resourceGroupName, zoneName, relativeRecordSetName, recordType, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockRecordSetsClient)(nil).Get), ctx, resourceGroupName, zoneName, relativeRecordSetName, recordType, options) +} + +// NewListAllByDNSZonePager mocks base method. +func (m *MockRecordSetsClient) NewListAllByDNSZonePager(resourceGroupName, zoneName string, options *armdns.RecordSetsClientListAllByDNSZoneOptions) clients.RecordSetsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListAllByDNSZonePager", resourceGroupName, zoneName, options) + ret0, _ := ret[0].(clients.RecordSetsPager) + return ret0 +} + +// NewListAllByDNSZonePager indicates an expected call of NewListAllByDNSZonePager. +func (mr *MockRecordSetsClientMockRecorder) NewListAllByDNSZonePager(resourceGroupName, zoneName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListAllByDNSZonePager", reflect.TypeOf((*MockRecordSetsClient)(nil).NewListAllByDNSZonePager), resourceGroupName, zoneName, options) +} diff --git a/sources/azure/shared/utils.go b/sources/azure/shared/utils.go index a6e3b09c..a06ed1a2 100644 --- a/sources/azure/shared/utils.go +++ b/sources/azure/shared/utils.go @@ -50,6 +50,7 @@ func GetResourceIDPathKeys(resourceType string) []string { "azure-network-security-rule": {"networkSecurityGroups", "securityRules"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/networkSecurityGroups/{nsgName}/securityRules/{ruleName}", "azure-batch-batch-application": {"batchAccounts", "applications"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Batch/batchAccounts/{accountName}/applications/{applicationName}", "azure-batch-batch-pool": {"batchAccounts", "pools"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Batch/batchAccounts/{accountName}/pools/{poolName}", + "azure-network-dns-record-set": {"dnszones"}, // "/subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Network/dnszones/{zoneName}/{recordType}/{relativeRecordSetName}" } if keys, ok := pathKeysMap[resourceType]; ok { @@ -121,6 +122,42 @@ func ExtractPathParamsFromResourceID(resourceID string, keys []string) []string return results } +// ExtractDNSRecordSetParamsFromResourceID extracts zone name, record type, and relative record set name +// from an Azure DNS record set resource ID. The path format is non-standard: after "dnszones" the next +// three segments are zoneName, recordType (e.g. "A", "AAAA"), and relativeRecordSetName—recordType is +// a value, not a path key, so ExtractPathParamsFromResourceID cannot be used. +// +// Example: .../dnszones/example.com/A/www returns ["example.com", "A", "www"]. +// Returns nil if the path does not match the expected structure. +func ExtractDNSRecordSetParamsFromResourceID(resourceID string) []string { + if resourceID == "" { + return nil + } + parts := strings.Split(strings.Trim(resourceID, "/"), "/") + for i, part := range parts { + if i%2 == 0 && strings.EqualFold(part, "dnszones") && i+3 < len(parts) { + return []string{parts[i+1], parts[i+2], parts[i+3]} + } + } + return nil +} + +// ExtractPathParamsFromResourceIDByType extracts query parts from an Azure resource ID for the given +// resource type. For azure-network-dns-record-set it uses ExtractDNSRecordSetParamsFromResourceID +// because the DNS path format (dnszones/zone/recordType/name) does not follow the usual key/value +// pattern. For all other types it uses GetResourceIDPathKeys and ExtractPathParamsFromResourceID. +// Returns nil if the type is unknown or extraction fails. +func ExtractPathParamsFromResourceIDByType(resourceType string, resourceID string) []string { + if resourceType == "azure-network-dns-record-set" { + return ExtractDNSRecordSetParamsFromResourceID(resourceID) + } + pathKeys := GetResourceIDPathKeys(resourceType) + if pathKeys == nil { + return nil + } + return ExtractPathParamsFromResourceID(resourceID, pathKeys) +} + // ExtractSQLServerNameFromDatabaseID extracts the SQL server name from a SQL database resource ID. // Azure SQL database IDs follow the format: // /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Sql/servers/{serverName}/databases/{databaseName} diff --git a/sources/azure/shared/utils_test.go b/sources/azure/shared/utils_test.go index 0fc91eaa..67cd2a8d 100644 --- a/sources/azure/shared/utils_test.go +++ b/sources/azure/shared/utils_test.go @@ -247,6 +247,73 @@ func TestExtractPathParamsFromResourceID(t *testing.T) { } } +func TestExtractDNSRecordSetParamsFromResourceID(t *testing.T) { + tests := []struct { + name string + resourceID string + expected []string + }{ + { + name: "valid DNS record set ID", + resourceID: "/subscriptions/sub-id/resourceGroups/rg/providers/Microsoft.Network/dnszones/example.com/A/www", + expected: []string{"example.com", "A", "www"}, + }, + { + name: "valid DNS record set ID - AAAA", + resourceID: "/subscriptions/sub-id/resourceGroups/rg/providers/Microsoft.Network/dnszones/zone.net/AAAA/mail", + expected: []string{"zone.net", "AAAA", "mail"}, + }, + { + name: "empty resource ID", + resourceID: "", + expected: nil, + }, + { + name: "no dnszones segment", + resourceID: "/subscriptions/sub-id/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet", + expected: nil, + }, + { + name: "dnszones but not enough segments after", + resourceID: "/subscriptions/sub-id/resourceGroups/rg/providers/Microsoft.Network/dnszones/example.com", + expected: nil, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + actual := azureshared.ExtractDNSRecordSetParamsFromResourceID(tc.resourceID) + if !reflect.DeepEqual(actual, tc.expected) { + t.Errorf("ExtractDNSRecordSetParamsFromResourceID(%q) = %v; want %v", tc.resourceID, actual, tc.expected) + } + }) + } +} + +func TestExtractPathParamsFromResourceIDByType(t *testing.T) { + t.Run("azure-network-dns-record-set uses DNS extractor", func(t *testing.T) { + resourceID := "/subscriptions/sub-id/resourceGroups/rg/providers/Microsoft.Network/dnszones/example.com/A/www" + actual := azureshared.ExtractPathParamsFromResourceIDByType("azure-network-dns-record-set", resourceID) + expected := []string{"example.com", "A", "www"} + if !reflect.DeepEqual(actual, expected) { + t.Errorf("ExtractPathParamsFromResourceIDByType(azure-network-dns-record-set, ...) = %v; want %v", actual, expected) + } + }) + t.Run("other type uses path keys", func(t *testing.T) { + resourceID := "/subscriptions/sub-id/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/myaccount/queueServices/default/queues/myqueue" + actual := azureshared.ExtractPathParamsFromResourceIDByType("azure-storage-queue", resourceID) + expected := []string{"myaccount", "myqueue"} + if !reflect.DeepEqual(actual, expected) { + t.Errorf("ExtractPathParamsFromResourceIDByType(azure-storage-queue, ...) = %v; want %v", actual, expected) + } + }) + t.Run("unknown type returns nil", func(t *testing.T) { + actual := azureshared.ExtractPathParamsFromResourceIDByType("azure-unknown-type", "/some/id") + if actual != nil { + t.Errorf("ExtractPathParamsFromResourceIDByType(unknown) = %v; want nil", actual) + } + }) +} + func TestConvertAzureTags(t *testing.T) { tests := []struct { name string diff --git a/sources/transformer.go b/sources/transformer.go index d114d7cd..cabedd51 100644 --- a/sources/transformer.go +++ b/sources/transformer.go @@ -612,9 +612,9 @@ func (s *standardSearchableAdapterImpl) Search(ctx context.Context, scope string // This must be a terraform query in Azure resource ID format: // /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{account}/queueServices/default/queues/{queue} // - // Extract the relevant parts from the resource ID based on the resource type - pathKeys := azureshared.GetResourceIDPathKeys(s.wrapper.Type()) - if pathKeys == nil { + // Extract the relevant parts from the resource ID based on the resource type. + // Distinguish "unknown type" (no path keys) from "extraction failed" (malformed or unsupported ID format). + if azureshared.GetResourceIDPathKeys(s.wrapper.Type()) == nil { return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_OTHER, ErrorString: fmt.Sprintf( @@ -624,8 +624,17 @@ func (s *standardSearchableAdapterImpl) Search(ctx context.Context, scope string ), } } - - queryParts = azureshared.ExtractPathParamsFromResourceID(query, pathKeys) + queryParts = azureshared.ExtractPathParamsFromResourceIDByType(s.wrapper.Type(), query) + if queryParts == nil { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: fmt.Sprintf( + "failed to extract query parts from resource ID for resource type %s (invalid or unsupported format): %s", + s.wrapper.Type(), + query, + ), + } + } if len(queryParts) != len(s.wrapper.GetLookups()) { return nil, &sdp.QueryError{ ErrorType: sdp.QueryError_OTHER, @@ -816,9 +825,9 @@ func (s *standardSearchableAdapterImpl) SearchStream(ctx context.Context, scope // This must be a terraform query in Azure resource ID format: // /subscriptions/{sub}/resourceGroups/{rg}/providers/Microsoft.Storage/storageAccounts/{account}/queueServices/default/queues/{queue} // - // Extract the relevant parts from the resource ID based on the resource type - pathKeys := azureshared.GetResourceIDPathKeys(s.wrapper.Type()) - if pathKeys == nil { + // Extract the relevant parts from the resource ID based on the resource type. + // Distinguish "unknown type" (no path keys) from "extraction failed" (malformed or unsupported ID format). + if azureshared.GetResourceIDPathKeys(s.wrapper.Type()) == nil { stream.SendError(&sdp.QueryError{ ErrorType: sdp.QueryError_OTHER, ErrorString: fmt.Sprintf( @@ -829,8 +838,18 @@ func (s *standardSearchableAdapterImpl) SearchStream(ctx context.Context, scope }) return } - - queryParts = azureshared.ExtractPathParamsFromResourceID(query, pathKeys) + queryParts = azureshared.ExtractPathParamsFromResourceIDByType(s.wrapper.Type(), query) + if queryParts == nil { + stream.SendError(&sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: fmt.Sprintf( + "failed to extract query parts from resource ID for resource type %s (invalid or unsupported format): %s", + s.wrapper.Type(), + query, + ), + }) + return + } if len(queryParts) != len(s.wrapper.GetLookups()) { stream.SendError(&sdp.QueryError{ ErrorType: sdp.QueryError_OTHER, From a9a38fe15695c7ba38f23e1d7855bb6b3ad8fe7e Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Wed, 4 Mar 2026 16:18:40 +0000 Subject: [PATCH 56/74] Eng 2204 create microsoftnetworkprivatednszones adapter (#4111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image --- > [!NOTE] > **Medium Risk** > Moderate risk: introduces a new Azure discovery adapter wired into the main adapter set and slightly changes linked-item query generation for `network-dns-record-set` TargetResource IDs, which could affect downstream graph/linking behavior. > > **Overview** > Adds first-class discovery for Azure **Private DNS Zones** via a new manual adapter (`network-private-dns-zone.go`) that supports `List`/`Get`, sets health from provisioning state, and emits links to stdlib DNS plus child resources (record sets and virtual network links). > > Wires the adapter into Azure manual initialization (`adapters.go`), including a new `armprivatedns` client wrapper (`clients/private-dns-zones-client.go`), dependency updates (`go.mod`/`go.sum`), generated gomock, and comprehensive unit tests. > > Also updates `network-dns-record-set.go` so `TargetResource` linking falls back to `ExtractResourceName` when path-key extraction isn’t available, avoiding missing/empty GET queries for simpler Azure resource IDs. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b7235563214325b7435c78c8c123b77537e5bb54. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: f18ab3f8621b48c462cdcfa4e3c5d18ed7a38519 --- go.mod | 5 + go.sum | 2 + .../azure/clients/private-dns-zones-client.go | 35 ++ sources/azure/manual/README.md | 4 + sources/azure/manual/adapters.go | 10 + .../azure/manual/network-dns-record-set.go | 31 +- .../azure/manual/network-private-dns-zone.go | 222 +++++++++++ .../manual/network-private-dns-zone_test.go | 376 ++++++++++++++++++ .../mocks/mock_private_dns_zones_client.go | 72 ++++ 9 files changed, 747 insertions(+), 10 deletions(-) create mode 100644 sources/azure/clients/private-dns-zones-client.go create mode 100644 sources/azure/manual/network-private-dns-zone.go create mode 100644 sources/azure/manual/network-private-dns-zone_test.go create mode 100644 sources/azure/shared/mocks/mock_private_dns_zones_client.go diff --git a/go.mod b/go.mod index 33b47b10..8190712f 100644 --- a/go.mod +++ b/go.mod @@ -520,3 +520,8 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.2 sigs.k8s.io/yaml v1.6.0 // indirect ) + +require ( + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect + github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect +) diff --git a/go.sum b/go.sum index 7673863e..bf6c7871 100644 --- a/go.sum +++ b/go.sum @@ -136,6 +136,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9 v9.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9 v9.0.0/go.mod h1:raqbEXrok4aycS74XoU6p9Hne1dliAFpHLizlp+qJoM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5 v5.0.0 h1:S7K+MLPEYe+g9AX9dLKldBpYV03bPl7zeDaWhiNDqqs= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5 v5.0.0/go.mod h1:EHRrmrnS2Q8fB3+DE30TTk04JLqjui5ZJEF7eMVQ2/M= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2 v2.1.0 h1:seyVIpxalxYmfjoo8MB4rRzWaobMG+KJ2+MAUrEvDGU= diff --git a/sources/azure/clients/private-dns-zones-client.go b/sources/azure/clients/private-dns-zones-client.go new file mode 100644 index 00000000..4ac42a61 --- /dev/null +++ b/sources/azure/clients/private-dns-zones-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" +) + +//go:generate mockgen -destination=../shared/mocks/mock_private_dns_zones_client.go -package=mocks -source=private-dns-zones-client.go + +// PrivateDNSZonesPager is a type alias for the generic Pager interface with private zone response type. +type PrivateDNSZonesPager = Pager[armprivatedns.PrivateZonesClientListByResourceGroupResponse] + +// PrivateDNSZonesClient is an interface for interacting with Azure Private DNS zones. +type PrivateDNSZonesClient interface { + NewListByResourceGroupPager(resourceGroupName string, options *armprivatedns.PrivateZonesClientListByResourceGroupOptions) PrivateDNSZonesPager + Get(ctx context.Context, resourceGroupName string, privateZoneName string, options *armprivatedns.PrivateZonesClientGetOptions) (armprivatedns.PrivateZonesClientGetResponse, error) +} + +type privateDNSZonesClient struct { + client *armprivatedns.PrivateZonesClient +} + +func (c *privateDNSZonesClient) NewListByResourceGroupPager(resourceGroupName string, options *armprivatedns.PrivateZonesClientListByResourceGroupOptions) PrivateDNSZonesPager { + return c.client.NewListByResourceGroupPager(resourceGroupName, options) +} + +func (c *privateDNSZonesClient) Get(ctx context.Context, resourceGroupName string, privateZoneName string, options *armprivatedns.PrivateZonesClientGetOptions) (armprivatedns.PrivateZonesClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, privateZoneName, options) +} + +// NewPrivateDNSZonesClient creates a new PrivateDNSZonesClient from the Azure SDK client. +func NewPrivateDNSZonesClient(client *armprivatedns.PrivateZonesClient) PrivateDNSZonesClient { + return &privateDNSZonesClient{client: client} +} diff --git a/sources/azure/manual/README.md b/sources/azure/manual/README.md index 324b0035..6834ec72 100644 --- a/sources/azure/manual/README.md +++ b/sources/azure/manual/README.md @@ -32,6 +32,10 @@ This directory contains manually implemented Azure adapters that cannot be gener - Network Security Groups: Referenced through network interfaces - Requires manual parsing and conditional linking based on the resource ID format and provider namespace +**Network Private DNS Zone** (`network-private-dns-zone.go`): +- Discovers Azure Private DNS Zones via `armprivatedns`; uses `MultiResourceGroupBase` and list-by-resource-group pager +- Links zone name to stdlib DNS for resolution; health from provisioning state + ## Implementation Guidelines ### For Detailed Implementation Rules diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 395549f0..b9d258be 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -10,6 +10,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" @@ -332,6 +333,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred if err != nil { return nil, fmt.Errorf("failed to create record sets client: %w", err) } + privateDNSZonesClient, err := armprivatedns.NewPrivateZonesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create private DNS 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) @@ -498,6 +503,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewZonesClient(zonesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewNetworkPrivateDNSZone( + clients.NewPrivateDNSZonesClient(privateDNSZonesClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewNetworkDNSRecordSet( clients.NewRecordSetsClient(recordSetsClient), resourceGroupScopes, @@ -686,6 +695,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewNetworkPublicIPAddress(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkLoadBalancer(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkZone(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewNetworkPrivateDNSZone(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkDNSRecordSet(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewBatchAccount(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewBatchBatchApplication(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/network-dns-record-set.go b/sources/azure/manual/network-dns-record-set.go index c2079691..00869b7e 100644 --- a/sources/azure/manual/network-dns-record-set.go +++ b/sources/azure/manual/network-dns-record-set.go @@ -313,6 +313,8 @@ func (n networkDNSRecordSetWrapper) azureRecordSetToSDPItem(rs *armdns.RecordSet // Pass the composite lookup key (extracted query parts) so the target adapter's Get // receives the expected parts when the transformer splits by QuerySeparator; it does // not parse full resource IDs for linked GET queries. + // For types in pathKeysMap we use ExtractPathParamsFromResourceIDByType; for simple + // single-name resources (e.g. public IP, Traffic Manager) we fall back to ExtractResourceName. if rs.Properties.TargetResource != nil && rs.Properties.TargetResource.ID != nil && *rs.Properties.TargetResource.ID != "" { targetID := *rs.Properties.TargetResource.ID linkScope := azureshared.ExtractScopeFromResourceID(targetID) @@ -320,16 +322,25 @@ func (n networkDNSRecordSetWrapper) azureRecordSetToSDPItem(rs *armdns.RecordSet linkScope = scope } itemType := azureshared.ItemTypeFromLinkedResourceID(targetID) - queryParts := azureshared.ExtractPathParamsFromResourceIDByType(itemType, targetID) - if itemType != "" && queryParts != nil { - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: itemType, - Method: sdp.QueryMethod_GET, - Query: shared.CompositeLookupKey(queryParts...), - Scope: linkScope, - }, - }) + if itemType != "" { + queryParts := azureshared.ExtractPathParamsFromResourceIDByType(itemType, targetID) + var query string + if queryParts != nil { + query = shared.CompositeLookupKey(queryParts...) + } else { + // Simple resource type (no pathKeysMap): use resource name as single query part + query = azureshared.ExtractResourceName(targetID) + } + if query != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: itemType, + Method: sdp.QueryMethod_GET, + Query: query, + Scope: linkScope, + }, + }) + } } } } diff --git a/sources/azure/manual/network-private-dns-zone.go b/sources/azure/manual/network-private-dns-zone.go new file mode 100644 index 00000000..18f54ba5 --- /dev/null +++ b/sources/azure/manual/network-private-dns-zone.go @@ -0,0 +1,222 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" + "github.com/overmindtech/cli/go/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 NetworkPrivateDNSZoneLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkPrivateDNSZone) + +type networkPrivateDNSZoneWrapper struct { + client clients.PrivateDNSZonesClient + + *azureshared.MultiResourceGroupBase +} + +func NewNetworkPrivateDNSZone(client clients.PrivateDNSZonesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &networkPrivateDNSZoneWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + azureshared.NetworkPrivateDNSZone, + ), + } +} + +func (n networkPrivateDNSZoneWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + pager := n.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, n.Type()) + } + for _, zone := range page.Value { + if zone.Name == nil { + continue + } + item, sdpErr := n.azurePrivateZoneToSDPItem(zone, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (n networkPrivateDNSZoneWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + pager := n.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + for _, zone := range page.Value { + if zone.Name == nil { + continue + } + item, sdpErr := n.azurePrivateZoneToSDPItem(zone, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (n networkPrivateDNSZoneWrapper) azurePrivateZoneToSDPItem(zone *armprivatedns.PrivateZone, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(zone, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + if zone.Name == nil { + return nil, azureshared.QueryError(errors.New("zone name is nil"), scope, n.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.NetworkPrivateDNSZone.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(zone.Tags), + } + + // Health from provisioning state + if zone.Properties != nil && zone.Properties.ProvisioningState != nil { + switch *zone.Properties.ProvisioningState { + case armprivatedns.ProvisioningStateSucceeded: + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case armprivatedns.ProvisioningStateCreating, armprivatedns.ProvisioningStateUpdating, armprivatedns.ProvisioningStateDeleting: + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case armprivatedns.ProvisioningStateFailed, armprivatedns.ProvisioningStateCanceled: + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + zoneName := *zone.Name + + // Link to DNS name (standard library) for the zone name + if zoneName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: zoneName, + Scope: "global", + }, + }) + } + + // Link to Virtual Network Links (child resource of Private DNS Zone) + // Reference: https://learn.microsoft.com/en-us/rest/api/dns/privatednszones/virtualnetworklinks/list + // Virtual network links can be listed by zone name, so we use SEARCH method + if zoneName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkDNSVirtualNetworkLink.String(), + Method: sdp.QueryMethod_SEARCH, + Query: zoneName, + Scope: scope, + }, + }) + } + + // Link to DNS Record Sets (child resource of Private DNS Zone) + // Reference: https://learn.microsoft.com/en-us/rest/api/dns/privatednszones/recordsets/list + // Record sets (A, AAAA, CNAME, MX, PTR, SOA, SRV, TXT) can be listed by zone name, so we use SEARCH method + if zoneName != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkDNSRecordSet.String(), + Method: sdp.QueryMethod_SEARCH, + Query: zoneName, + Scope: scope, + }, + }) + } + + return sdpItem, nil +} + +// ref: https://learn.microsoft.com/en-us/rest/api/dns/privatednszones/get +func (n networkPrivateDNSZoneWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, azureshared.QueryError(errors.New("query must be exactly one part (private zone name)"), scope, n.Type()) + } + zoneName := queryParts[0] + if zoneName == "" { + return nil, azureshared.QueryError(errors.New("private zone name cannot be empty"), scope, n.Type()) + } + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + resp, err := n.client.Get(ctx, rgScope.ResourceGroup, zoneName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + return n.azurePrivateZoneToSDPItem(&resp.PrivateZone, scope) +} + +func (n networkPrivateDNSZoneWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + NetworkPrivateDNSZoneLookupByName, + } +} + +func (n networkPrivateDNSZoneWrapper) PotentialLinks() map[shared.ItemType]bool { + return shared.NewItemTypesSet( + azureshared.NetworkDNSRecordSet, + azureshared.NetworkDNSVirtualNetworkLink, + stdlib.NetworkDNS, + ) +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_dns_zone +func (n networkPrivateDNSZoneWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_private_dns_zone.name", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/resource-provider-operations#microsoftnetwork +func (n networkPrivateDNSZoneWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Network/privateDnsZones/read", + } +} + +func (n networkPrivateDNSZoneWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/network-private-dns-zone_test.go b/sources/azure/manual/network-private-dns-zone_test.go new file mode 100644 index 00000000..4901d7a3 --- /dev/null +++ b/sources/azure/manual/network-private-dns-zone_test.go @@ -0,0 +1,376 @@ +package manual_test + +import ( + "context" + "errors" + "slices" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" + "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 TestNetworkPrivateDNSZone(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) { + zoneName := "private.example.com" + zone := createAzurePrivateZone(zoneName) + + mockClient := mocks.NewMockPrivateDNSZonesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, zoneName, nil).Return( + armprivatedns.PrivateZonesClientGetResponse{ + PrivateZone: *zone, + }, nil) + + wrapper := manual.NewNetworkPrivateDNSZone(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], zoneName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.NetworkPrivateDNSZone.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkPrivateDNSZone, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != zoneName { + t.Errorf("Expected unique attribute value %s, got %s", zoneName, 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) { + scope := subscriptionID + "." + resourceGroup + queryTests := shared.QueryTests{ + { + ExpectedType: stdlib.NetworkDNS.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: zoneName, + ExpectedScope: "global", + }, + { + ExpectedType: azureshared.NetworkDNSVirtualNetworkLink.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: zoneName, + ExpectedScope: scope, + }, + { + ExpectedType: azureshared.NetworkDNSRecordSet.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: zoneName, + ExpectedScope: scope, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockPrivateDNSZonesClient(ctrl) + + wrapper := manual.NewNetworkPrivateDNSZone(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 zone name is empty, but got nil") + } + }) + + t.Run("Get_ZoneWithNilName", func(t *testing.T) { + provisioningState := armprivatedns.ProvisioningStateSucceeded + zoneWithNilName := &armprivatedns.PrivateZone{ + Name: nil, + Location: new("eastus"), + Properties: &armprivatedns.PrivateZoneProperties{ + ProvisioningState: &provisioningState, + }, + } + + mockClient := mocks.NewMockPrivateDNSZonesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "test-zone", nil).Return( + armprivatedns.PrivateZonesClientGetResponse{ + PrivateZone: *zoneWithNilName, + }, nil) + + wrapper := manual.NewNetworkPrivateDNSZone(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-zone", true) + if qErr == nil { + t.Error("Expected error when zone has nil name, but got nil") + } + }) + + t.Run("List", func(t *testing.T) { + zone1 := createAzurePrivateZone("private1.example.com") + zone2 := createAzurePrivateZone("private2.example.com") + + mockClient := mocks.NewMockPrivateDNSZonesClient(ctrl) + mockPager := newMockPrivateDNSZonesPager(ctrl, []*armprivatedns.PrivateZone{zone1, zone2}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkPrivateDNSZone(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.GetType() != azureshared.NetworkPrivateDNSZone.String() { + t.Fatalf("Expected type %s, got: %s", azureshared.NetworkPrivateDNSZone, item.GetType()) + } + } + }) + + t.Run("List_WithNilName", func(t *testing.T) { + zone1 := createAzurePrivateZone("private1.example.com") + provisioningState := armprivatedns.ProvisioningStateSucceeded + zone2NilName := &armprivatedns.PrivateZone{ + Name: nil, + Location: new("eastus"), + Tags: map[string]*string{"env": new("test")}, + Properties: &armprivatedns.PrivateZoneProperties{ + ProvisioningState: &provisioningState, + }, + } + + mockClient := mocks.NewMockPrivateDNSZonesClient(ctrl) + mockPager := newMockPrivateDNSZonesPager(ctrl, []*armprivatedns.PrivateZone{zone1, zone2NilName}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkPrivateDNSZone(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)) + } + if sdpItems[0].UniqueAttributeValue() != "private1.example.com" { + t.Errorf("Expected item name 'private1.example.com', got: %s", sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("ListStream", func(t *testing.T) { + zone1 := createAzurePrivateZone("stream1.example.com") + zone2 := createAzurePrivateZone("stream2.example.com") + + mockClient := mocks.NewMockPrivateDNSZonesClient(ctrl) + mockPager := newMockPrivateDNSZonesPager(ctrl, []*armprivatedns.PrivateZone{zone1, zone2}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkPrivateDNSZone(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)) + } + }) + + t.Run("ErrorHandling", func(t *testing.T) { + expectedErr := errors.New("private zone not found") + + mockClient := mocks.NewMockPrivateDNSZonesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent-zone", nil).Return( + armprivatedns.PrivateZonesClientGetResponse{}, expectedErr) + + wrapper := manual.NewNetworkPrivateDNSZone(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "nonexistent-zone", true) + if qErr == nil { + t.Error("Expected error when getting non-existent zone, but got nil") + } + }) + + t.Run("InterfaceCompliance", func(t *testing.T) { + mockClient := mocks.NewMockPrivateDNSZonesClient(ctrl) + wrapper := manual.NewNetworkPrivateDNSZone(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + w := wrapper.(sources.Wrapper) + + permissions := w.IAMPermissions() + if len(permissions) == 0 { + t.Error("Expected IAMPermissions to return at least one permission") + } + expectedPermission := "Microsoft.Network/privateDnsZones/read" + if !slices.Contains(permissions, expectedPermission) { + t.Errorf("Expected IAMPermissions to include %s", expectedPermission) + } + + potentialLinks := w.PotentialLinks() + if !potentialLinks[azureshared.NetworkDNSRecordSet] { + t.Error("Expected PotentialLinks to include NetworkDNSRecordSet") + } + if !potentialLinks[azureshared.NetworkDNSVirtualNetworkLink] { + t.Error("Expected PotentialLinks to include NetworkDNSVirtualNetworkLink") + } + if !potentialLinks[stdlib.NetworkDNS] { + t.Error("Expected PotentialLinks to include stdlib.NetworkDNS") + } + + mappings := w.TerraformMappings() + foundMapping := false + for _, mapping := range mappings { + if mapping.GetTerraformQueryMap() == "azurerm_private_dns_zone.name" { + foundMapping = true + if mapping.GetTerraformMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected TerraformMethod GET, got: %s", mapping.GetTerraformMethod()) + } + break + } + } + if !foundMapping { + t.Error("Expected TerraformMappings to include 'azurerm_private_dns_zone.name'") + } + + lookups := w.GetLookups() + foundLookup := false + for _, lookup := range lookups { + if lookup.ItemType == azureshared.NetworkPrivateDNSZone { + foundLookup = true + break + } + } + if !foundLookup { + t.Error("Expected GetLookups to include NetworkPrivateDNSZone") + } + }) +} + +type mockPrivateDNSZonesPager struct { + ctrl *gomock.Controller + items []*armprivatedns.PrivateZone + index int + more bool +} + +func newMockPrivateDNSZonesPager(ctrl *gomock.Controller, items []*armprivatedns.PrivateZone) clients.PrivateDNSZonesPager { + return &mockPrivateDNSZonesPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockPrivateDNSZonesPager) More() bool { + return m.more +} + +func (m *mockPrivateDNSZonesPager) NextPage(ctx context.Context) (armprivatedns.PrivateZonesClientListByResourceGroupResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armprivatedns.PrivateZonesClientListByResourceGroupResponse{ + PrivateZoneListResult: armprivatedns.PrivateZoneListResult{ + Value: []*armprivatedns.PrivateZone{}, + }, + }, nil + } + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + return armprivatedns.PrivateZonesClientListByResourceGroupResponse{ + PrivateZoneListResult: armprivatedns.PrivateZoneListResult{ + Value: []*armprivatedns.PrivateZone{item}, + }, + }, nil +} + +func createAzurePrivateZone(zoneName string) *armprivatedns.PrivateZone { + state := armprivatedns.ProvisioningStateSucceeded + return &armprivatedns.PrivateZone{ + ID: new("/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Network/privateDnsZones/" + zoneName), + Name: new(zoneName), + Type: new("Microsoft.Network/privateDnsZones"), + Location: new("global"), + Tags: map[string]*string{ + "env": new("test"), + "project": new("testing"), + }, + Properties: &armprivatedns.PrivateZoneProperties{ + ProvisioningState: &state, + MaxNumberOfRecordSets: new(int64(5000)), + NumberOfRecordSets: new(int64(0)), + }, + } +} + +// Ensure mockPrivateDNSZonesPager satisfies the pager interface at compile time. +var _ clients.PrivateDNSZonesPager = (*mockPrivateDNSZonesPager)(nil) diff --git a/sources/azure/shared/mocks/mock_private_dns_zones_client.go b/sources/azure/shared/mocks/mock_private_dns_zones_client.go new file mode 100644 index 00000000..4e0a6f86 --- /dev/null +++ b/sources/azure/shared/mocks/mock_private_dns_zones_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: private-dns-zones-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_private_dns_zones_client.go -package=mocks -source=private-dns-zones-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armprivatedns "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockPrivateDNSZonesClient is a mock of PrivateDNSZonesClient interface. +type MockPrivateDNSZonesClient struct { + ctrl *gomock.Controller + recorder *MockPrivateDNSZonesClientMockRecorder + isgomock struct{} +} + +// MockPrivateDNSZonesClientMockRecorder is the mock recorder for MockPrivateDNSZonesClient. +type MockPrivateDNSZonesClientMockRecorder struct { + mock *MockPrivateDNSZonesClient +} + +// NewMockPrivateDNSZonesClient creates a new mock instance. +func NewMockPrivateDNSZonesClient(ctrl *gomock.Controller) *MockPrivateDNSZonesClient { + mock := &MockPrivateDNSZonesClient{ctrl: ctrl} + mock.recorder = &MockPrivateDNSZonesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPrivateDNSZonesClient) EXPECT() *MockPrivateDNSZonesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockPrivateDNSZonesClient) Get(ctx context.Context, resourceGroupName, privateZoneName string, options *armprivatedns.PrivateZonesClientGetOptions) (armprivatedns.PrivateZonesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, privateZoneName, options) + ret0, _ := ret[0].(armprivatedns.PrivateZonesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockPrivateDNSZonesClientMockRecorder) Get(ctx, resourceGroupName, privateZoneName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPrivateDNSZonesClient)(nil).Get), ctx, resourceGroupName, privateZoneName, options) +} + +// NewListByResourceGroupPager mocks base method. +func (m *MockPrivateDNSZonesClient) NewListByResourceGroupPager(resourceGroupName string, options *armprivatedns.PrivateZonesClientListByResourceGroupOptions) clients.PrivateDNSZonesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByResourceGroupPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.PrivateDNSZonesPager) + return ret0 +} + +// NewListByResourceGroupPager indicates an expected call of NewListByResourceGroupPager. +func (mr *MockPrivateDNSZonesClientMockRecorder) NewListByResourceGroupPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByResourceGroupPager", reflect.TypeOf((*MockPrivateDNSZonesClient)(nil).NewListByResourceGroupPager), resourceGroupName, options) +} From 2a44d7cb37f61dd7f8eb9ae0816c92a72e63f6d7 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Wed, 4 Mar 2026 19:42:45 +0100 Subject: [PATCH 57/74] Implement Azure Application Security Groups Client and Adapter (#4114) image --- > [!NOTE] > **Medium Risk** > Adds a new Azure network resource adapter and wires it into adapter initialization, plus changes core adapter metadata generation to always emit an explicit (possibly empty) `PotentialLinks` list, which may affect downstream metadata consumers. > > **Overview** > Adds discovery support for Azure **Application Security Groups** by introducing a new `ApplicationSecurityGroupsClient` wrapper, a `NetworkApplicationSecurityGroup` manual adapter (Get/List/ListStream), and accompanying gomock + unit tests. > > Wires the new adapter into `manual/adapters.go` by instantiating the Azure SDK ASG client and registering the adapter in both real and placeholder (metadata-only) adapter lists. > > Fixes adapter metadata generation in `sources/transformer.go` to initialize `AdapterMetadata.PotentialLinks` to an empty slice whenever `PotentialLinks()` is non-nil, avoiding a nil field when wrappers return an empty map. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 2dd6323205b24e4885bcf5ecceadbd6026e1a2f5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: d0701a1694a97679bb58058cf65f8ebe514ac47f --- .../application-security-groups-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + .../network-application-security-group.go | 178 +++++++++ ...network-application-security-group_test.go | 344 ++++++++++++++++++ ...mock_application_security_groups_client.go | 72 ++++ sources/transformer.go | 4 + 6 files changed, 643 insertions(+) create mode 100644 sources/azure/clients/application-security-groups-client.go create mode 100644 sources/azure/manual/network-application-security-group.go create mode 100644 sources/azure/manual/network-application-security-group_test.go create mode 100644 sources/azure/shared/mocks/mock_application_security_groups_client.go diff --git a/sources/azure/clients/application-security-groups-client.go b/sources/azure/clients/application-security-groups-client.go new file mode 100644 index 00000000..18fcdb09 --- /dev/null +++ b/sources/azure/clients/application-security-groups-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" +) + +//go:generate mockgen -destination=../shared/mocks/mock_application_security_groups_client.go -package=mocks -source=application-security-groups-client.go + +// ApplicationSecurityGroupsPager is a type alias for the generic Pager interface with application security group response type. +type ApplicationSecurityGroupsPager = Pager[armnetwork.ApplicationSecurityGroupsClientListResponse] + +// ApplicationSecurityGroupsClient is an interface for interacting with Azure application security groups. +type ApplicationSecurityGroupsClient interface { + Get(ctx context.Context, resourceGroupName string, applicationSecurityGroupName string, options *armnetwork.ApplicationSecurityGroupsClientGetOptions) (armnetwork.ApplicationSecurityGroupsClientGetResponse, error) + NewListPager(resourceGroupName string, options *armnetwork.ApplicationSecurityGroupsClientListOptions) ApplicationSecurityGroupsPager +} + +type applicationSecurityGroupsClient struct { + client *armnetwork.ApplicationSecurityGroupsClient +} + +func (c *applicationSecurityGroupsClient) Get(ctx context.Context, resourceGroupName string, applicationSecurityGroupName string, options *armnetwork.ApplicationSecurityGroupsClientGetOptions) (armnetwork.ApplicationSecurityGroupsClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, applicationSecurityGroupName, options) +} + +func (c *applicationSecurityGroupsClient) NewListPager(resourceGroupName string, options *armnetwork.ApplicationSecurityGroupsClientListOptions) ApplicationSecurityGroupsPager { + return c.client.NewListPager(resourceGroupName, options) +} + +// NewApplicationSecurityGroupsClient creates a new ApplicationSecurityGroupsClient from the Azure SDK client. +func NewApplicationSecurityGroupsClient(client *armnetwork.ApplicationSecurityGroupsClient) ApplicationSecurityGroupsClient { + return &applicationSecurityGroupsClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index b9d258be..750f1b54 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -231,6 +231,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create application gateways client: %w", err) } + applicationSecurityGroupsClient, err := armnetwork.NewApplicationSecurityGroupsClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create application security groups client: %w", err) + } + managedHSMsClient, err := armkeyvault.NewManagedHsmsClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create managed hsms client: %w", err) @@ -539,6 +544,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewNetworkSecurityGroupsClient(networkSecurityGroupsClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewNetworkApplicationSecurityGroup( + clients.NewApplicationSecurityGroupsClient(applicationSecurityGroupsClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewNetworkRouteTable( clients.NewRouteTablesClient(routeTablesClient), resourceGroupScopes, @@ -704,6 +713,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewComputeAvailabilitySet(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewComputeDisk(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkNetworkSecurityGroup(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewNetworkApplicationSecurityGroup(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkSecurityRule(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkRouteTable(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkApplicationGateway(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/network-application-security-group.go b/sources/azure/manual/network-application-security-group.go new file mode 100644 index 00000000..6aec9794 --- /dev/null +++ b/sources/azure/manual/network-application-security-group.go @@ -0,0 +1,178 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +var NetworkApplicationSecurityGroupLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkApplicationSecurityGroup) + +type networkApplicationSecurityGroupWrapper struct { + client clients.ApplicationSecurityGroupsClient + + *azureshared.MultiResourceGroupBase +} + +func NewNetworkApplicationSecurityGroup(client clients.ApplicationSecurityGroupsClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &networkApplicationSecurityGroupWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + azureshared.NetworkApplicationSecurityGroup, + ), + } +} + +func (n networkApplicationSecurityGroupWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + pager := n.client.NewListPager(rgScope.ResourceGroup, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + for _, asg := range page.Value { + if asg.Name == nil { + continue + } + item, sdpErr := n.azureApplicationSecurityGroupToSDPItem(asg, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (n networkApplicationSecurityGroupWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + pager := n.client.NewListPager(rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + for _, asg := range page.Value { + if asg.Name == nil { + continue + } + item, sdpErr := n.azureApplicationSecurityGroupToSDPItem(asg, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (n networkApplicationSecurityGroupWrapper) azureApplicationSecurityGroupToSDPItem(asg *armnetwork.ApplicationSecurityGroup, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(asg, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + if asg.Name == nil { + return nil, azureshared.QueryError(errors.New("application security group name is nil"), scope, n.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.NetworkApplicationSecurityGroup.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(asg.Tags), + LinkedItemQueries: []*sdp.LinkedItemQuery{}, + } + + //no links - https://learn.microsoft.com/en-us/rest/api/virtualnetwork/application-security-groups/get?view=rest-virtualnetwork-2025-05-01&tabs=HTTP + + // Health from provisioning state + if asg.Properties != nil && asg.Properties.ProvisioningState != nil { + switch *asg.Properties.ProvisioningState { + case armnetwork.ProvisioningStateSucceeded: + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case armnetwork.ProvisioningStateCreating, armnetwork.ProvisioningStateUpdating, armnetwork.ProvisioningStateDeleting: + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case armnetwork.ProvisioningStateFailed, armnetwork.ProvisioningStateCanceled: + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + return sdpItem, nil +} + +// ref: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/application-security-groups/get +func (n networkApplicationSecurityGroupWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, azureshared.QueryError(errors.New("query must be exactly one part (application security group name)"), scope, n.Type()) + } + asgName := queryParts[0] + if asgName == "" { + return nil, azureshared.QueryError(errors.New("application security group name cannot be empty"), scope, n.Type()) + } + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + resp, err := n.client.Get(ctx, rgScope.ResourceGroup, asgName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + return n.azureApplicationSecurityGroupToSDPItem(&resp.ApplicationSecurityGroup, scope) +} + +func (n networkApplicationSecurityGroupWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + NetworkApplicationSecurityGroupLookupByName, + } +} + +func (n networkApplicationSecurityGroupWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{} +} + +// ref: https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/application_security_group +func (n networkApplicationSecurityGroupWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_application_security_group.name", + }, + } +} + +// ref: https://learn.microsoft.com/en-us/azure/role-based-access-control/resource-provider-operations#microsoftnetwork +func (n networkApplicationSecurityGroupWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Network/applicationSecurityGroups/read", + } +} + +func (n networkApplicationSecurityGroupWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/network-application-security-group_test.go b/sources/azure/manual/network-application-security-group_test.go new file mode 100644 index 00000000..7fb3c306 --- /dev/null +++ b/sources/azure/manual/network-application-security-group_test.go @@ -0,0 +1,344 @@ +package manual_test + +import ( + "context" + "errors" + "slices" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "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 TestNetworkApplicationSecurityGroup(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) { + asgName := "test-asg" + asg := createAzureApplicationSecurityGroup(asgName) + + mockClient := mocks.NewMockApplicationSecurityGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, asgName, nil).Return( + armnetwork.ApplicationSecurityGroupsClientGetResponse{ + ApplicationSecurityGroup: *asg, + }, nil) + + wrapper := manual.NewNetworkApplicationSecurityGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], asgName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.NetworkApplicationSecurityGroup.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkApplicationSecurityGroup, sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != asgName { + t.Errorf("Expected unique attribute value %s, got %s", asgName, 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) { + // Application Security Group has no linked item queries + queryTests := shared.QueryTests{} + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockApplicationSecurityGroupsClient(ctrl) + + wrapper := manual.NewNetworkApplicationSecurityGroup(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 application security group name is empty, but got nil") + } + }) + + t.Run("Get_ASGWithNilName", func(t *testing.T) { + provisioningState := armnetwork.ProvisioningStateSucceeded + asgWithNilName := &armnetwork.ApplicationSecurityGroup{ + Name: nil, + Location: new("eastus"), + Properties: &armnetwork.ApplicationSecurityGroupPropertiesFormat{ + ProvisioningState: &provisioningState, + }, + } + + mockClient := mocks.NewMockApplicationSecurityGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "test-asg", nil).Return( + armnetwork.ApplicationSecurityGroupsClientGetResponse{ + ApplicationSecurityGroup: *asgWithNilName, + }, nil) + + wrapper := manual.NewNetworkApplicationSecurityGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-asg", true) + if qErr == nil { + t.Error("Expected error when application security group has nil name, but got nil") + } + }) + + t.Run("List", func(t *testing.T) { + asg1 := createAzureApplicationSecurityGroup("asg-1") + asg2 := createAzureApplicationSecurityGroup("asg-2") + + mockClient := mocks.NewMockApplicationSecurityGroupsClient(ctrl) + mockPager := newMockApplicationSecurityGroupsPager(ctrl, []*armnetwork.ApplicationSecurityGroup{asg1, asg2}) + + mockClient.EXPECT().NewListPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkApplicationSecurityGroup(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.GetType() != azureshared.NetworkApplicationSecurityGroup.String() { + t.Fatalf("Expected type %s, got: %s", azureshared.NetworkApplicationSecurityGroup, item.GetType()) + } + } + }) + + t.Run("List_WithNilName", func(t *testing.T) { + asg1 := createAzureApplicationSecurityGroup("asg-1") + provisioningState := armnetwork.ProvisioningStateSucceeded + asg2NilName := &armnetwork.ApplicationSecurityGroup{ + Name: nil, + Location: new("eastus"), + Tags: map[string]*string{"env": new("test")}, + Properties: &armnetwork.ApplicationSecurityGroupPropertiesFormat{ + ProvisioningState: &provisioningState, + }, + } + + mockClient := mocks.NewMockApplicationSecurityGroupsClient(ctrl) + mockPager := newMockApplicationSecurityGroupsPager(ctrl, []*armnetwork.ApplicationSecurityGroup{asg1, asg2NilName}) + + mockClient.EXPECT().NewListPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkApplicationSecurityGroup(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)) + } + if sdpItems[0].UniqueAttributeValue() != "asg-1" { + t.Errorf("Expected item name 'asg-1', got: %s", sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("ListStream", func(t *testing.T) { + asg1 := createAzureApplicationSecurityGroup("stream-asg-1") + asg2 := createAzureApplicationSecurityGroup("stream-asg-2") + + mockClient := mocks.NewMockApplicationSecurityGroupsClient(ctrl) + mockPager := newMockApplicationSecurityGroupsPager(ctrl, []*armnetwork.ApplicationSecurityGroup{asg1, asg2}) + + mockClient.EXPECT().NewListPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkApplicationSecurityGroup(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)) + } + }) + + t.Run("ErrorHandling", func(t *testing.T) { + expectedErr := errors.New("application security group not found") + + mockClient := mocks.NewMockApplicationSecurityGroupsClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent-asg", nil).Return( + armnetwork.ApplicationSecurityGroupsClientGetResponse{}, expectedErr) + + wrapper := manual.NewNetworkApplicationSecurityGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "nonexistent-asg", true) + if qErr == nil { + t.Error("Expected error when getting non-existent application security group, but got nil") + } + }) + + t.Run("InterfaceCompliance", func(t *testing.T) { + mockClient := mocks.NewMockApplicationSecurityGroupsClient(ctrl) + wrapper := manual.NewNetworkApplicationSecurityGroup(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + w := wrapper.(sources.Wrapper) + + permissions := w.IAMPermissions() + if len(permissions) == 0 { + t.Error("Expected IAMPermissions to return at least one permission") + } + expectedPermission := "Microsoft.Network/applicationSecurityGroups/read" + if !slices.Contains(permissions, expectedPermission) { + t.Errorf("Expected IAMPermissions to include %s", expectedPermission) + } + + mappings := w.TerraformMappings() + foundMapping := false + for _, mapping := range mappings { + if mapping.GetTerraformQueryMap() == "azurerm_application_security_group.name" { + foundMapping = true + if mapping.GetTerraformMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected TerraformMethod GET, got: %s", mapping.GetTerraformMethod()) + } + break + } + } + if !foundMapping { + t.Error("Expected TerraformMappings to include 'azurerm_application_security_group.name'") + } + + lookups := w.GetLookups() + foundLookup := false + for _, lookup := range lookups { + if lookup.ItemType == azureshared.NetworkApplicationSecurityGroup { + foundLookup = true + break + } + } + if !foundLookup { + t.Error("Expected GetLookups to include NetworkApplicationSecurityGroup") + } + }) +} + +type mockApplicationSecurityGroupsPager struct { + ctrl *gomock.Controller + items []*armnetwork.ApplicationSecurityGroup + index int + more bool +} + +func newMockApplicationSecurityGroupsPager(ctrl *gomock.Controller, items []*armnetwork.ApplicationSecurityGroup) clients.ApplicationSecurityGroupsPager { + return &mockApplicationSecurityGroupsPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockApplicationSecurityGroupsPager) More() bool { + return m.more +} + +func (m *mockApplicationSecurityGroupsPager) NextPage(ctx context.Context) (armnetwork.ApplicationSecurityGroupsClientListResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armnetwork.ApplicationSecurityGroupsClientListResponse{ + ApplicationSecurityGroupListResult: armnetwork.ApplicationSecurityGroupListResult{ + Value: []*armnetwork.ApplicationSecurityGroup{}, + }, + }, nil + } + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + return armnetwork.ApplicationSecurityGroupsClientListResponse{ + ApplicationSecurityGroupListResult: armnetwork.ApplicationSecurityGroupListResult{ + Value: []*armnetwork.ApplicationSecurityGroup{item}, + }, + }, nil +} + +func createAzureApplicationSecurityGroup(name string) *armnetwork.ApplicationSecurityGroup { + provisioningState := armnetwork.ProvisioningStateSucceeded + return &armnetwork.ApplicationSecurityGroup{ + ID: new("/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Network/applicationSecurityGroups/" + name), + Name: new(name), + Type: new("Microsoft.Network/applicationSecurityGroups"), + Location: new("eastus"), + Tags: map[string]*string{ + "env": new("test"), + "project": new("testing"), + }, + Properties: &armnetwork.ApplicationSecurityGroupPropertiesFormat{ + ProvisioningState: &provisioningState, + ResourceGUID: new("00000000-0000-0000-0000-000000000001"), + }, + } +} + +// Ensure mockApplicationSecurityGroupsPager satisfies the pager interface at compile time. +var _ clients.ApplicationSecurityGroupsPager = (*mockApplicationSecurityGroupsPager)(nil) diff --git a/sources/azure/shared/mocks/mock_application_security_groups_client.go b/sources/azure/shared/mocks/mock_application_security_groups_client.go new file mode 100644 index 00000000..0df8c08d --- /dev/null +++ b/sources/azure/shared/mocks/mock_application_security_groups_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: application-security-groups-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_application_security_groups_client.go -package=mocks -source=application-security-groups-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockApplicationSecurityGroupsClient is a mock of ApplicationSecurityGroupsClient interface. +type MockApplicationSecurityGroupsClient struct { + ctrl *gomock.Controller + recorder *MockApplicationSecurityGroupsClientMockRecorder + isgomock struct{} +} + +// MockApplicationSecurityGroupsClientMockRecorder is the mock recorder for MockApplicationSecurityGroupsClient. +type MockApplicationSecurityGroupsClientMockRecorder struct { + mock *MockApplicationSecurityGroupsClient +} + +// NewMockApplicationSecurityGroupsClient creates a new mock instance. +func NewMockApplicationSecurityGroupsClient(ctrl *gomock.Controller) *MockApplicationSecurityGroupsClient { + mock := &MockApplicationSecurityGroupsClient{ctrl: ctrl} + mock.recorder = &MockApplicationSecurityGroupsClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockApplicationSecurityGroupsClient) EXPECT() *MockApplicationSecurityGroupsClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockApplicationSecurityGroupsClient) Get(ctx context.Context, resourceGroupName, applicationSecurityGroupName string, options *armnetwork.ApplicationSecurityGroupsClientGetOptions) (armnetwork.ApplicationSecurityGroupsClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, applicationSecurityGroupName, options) + ret0, _ := ret[0].(armnetwork.ApplicationSecurityGroupsClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockApplicationSecurityGroupsClientMockRecorder) Get(ctx, resourceGroupName, applicationSecurityGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockApplicationSecurityGroupsClient)(nil).Get), ctx, resourceGroupName, applicationSecurityGroupName, options) +} + +// NewListPager mocks base method. +func (m *MockApplicationSecurityGroupsClient) NewListPager(resourceGroupName string, options *armnetwork.ApplicationSecurityGroupsClientListOptions) clients.ApplicationSecurityGroupsPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.ApplicationSecurityGroupsPager) + return ret0 +} + +// NewListPager indicates an expected call of NewListPager. +func (mr *MockApplicationSecurityGroupsClientMockRecorder) NewListPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListPager", reflect.TypeOf((*MockApplicationSecurityGroupsClient)(nil).NewListPager), resourceGroupName, options) +} diff --git a/sources/transformer.go b/sources/transformer.go index cabedd51..6cca672a 100644 --- a/sources/transformer.go +++ b/sources/transformer.go @@ -367,6 +367,7 @@ func (s *standardAdapterImpl) Metadata() *sdp.AdapterMetadata { } if s.wrapper.PotentialLinks() != nil { + a.PotentialLinks = []string{} for link := range s.wrapper.PotentialLinks() { a.PotentialLinks = append(a.PotentialLinks, link.String()) } @@ -539,6 +540,7 @@ func (s *standardListableAdapterImpl) Metadata() *sdp.AdapterMetadata { } if s.wrapper.PotentialLinks() != nil { + a.PotentialLinks = []string{} for link := range s.wrapper.PotentialLinks() { a.PotentialLinks = append(a.PotentialLinks, link.String()) } @@ -965,6 +967,7 @@ func (s *standardSearchableAdapterImpl) Metadata() *sdp.AdapterMetadata { } if s.wrapper.PotentialLinks() != nil { + a.PotentialLinks = []string{} for link := range s.wrapper.PotentialLinks() { a.PotentialLinks = append(a.PotentialLinks, link.String()) } @@ -1024,6 +1027,7 @@ func (s *standardSearchableListableAdapterImpl) Metadata() *sdp.AdapterMetadata } if s.wrapper.PotentialLinks() != nil { + a.PotentialLinks = []string{} for link := range s.wrapper.PotentialLinks() { a.PotentialLinks = append(a.PotentialLinks, link.String()) } From 777d787d81eb0051c45ffec6212fec7073aebb5f Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Wed, 4 Mar 2026 23:45:46 +0100 Subject: [PATCH 58/74] [ENG-2935] Fix captureGoroutineSummary thundering herd with singleflight (#4119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Wrap `captureGoroutineSummary` in `singleflight.Group` so that when many ExecuteQuery goroutines hit the stuck timeout simultaneously, only one runs the stop-the-world pprof capture - Only the goroutine that performed the capture includes the full profile string in its span event; others emit just the numeric counts — reducing OTel data from 32K × 48KB to 1 × 48KB per stuck event window ## Linear Ticket - **Ticket**: [ENG-2935](https://linear.app/overmind/issue/ENG-2935/add-tracing-instrumentation-for-source-waitgroups-stuck-diagnosis) — Add tracing instrumentation for Source WaitGroups stuck diagnosis - **Purpose**: Fix a thundering herd introduced by #4089 (merged), observed in the first production goroutine dump ## Context A production goroutine dump captured by the new instrumentation showed **32,189 goroutines** simultaneously inside `captureGoroutineSummary`, each calling `pprof.Lookup("goroutine").WriteTo()`. This is a stop-the-world operation that serializes all goroutine stacks — having 32K concurrent instances is catastrophic and amplifies the stuck condition. ## Changes `go/discovery/enginerequests.go`: - Add a package-level `singleflight.Group` (`goroutineProfileGroup`) - `captureGoroutineSummary` now calls `group.Do("goroutine-profile", ...)` — concurrent callers share the result - Returns a `shared` bool; the call site only includes `ovm.stuck.goroutineProfile` in the span event when `shared == false` - Fresh captures still happen for subsequent (non-concurrent) stuck events ## Deviations from Approved Plan This is a follow-up fix not covered by the original plan. The thundering herd was discovered from the first production goroutine dump after the plan was deployed. Made with [Cursor](https://cursor.com) --- > [!NOTE] > **Low Risk** > Changes are limited to stuck-diagnostics instrumentation in `ExecuteQuery`, reducing profiling/telemetry overhead without affecting normal query execution logic. Main risk is reduced per-span profile detail when multiple timeouts occur concurrently. > > **Overview** > Prevents a *thundering herd* when many `ExecuteQuery` calls hit the stuck waitgroup timeout by wrapping goroutine profile capture (`pprof.Lookup("goroutine")`) in a package-level `singleflight.Group`, so concurrent callers share one capture. > > Adds `compactGoroutineProfile` to strip noisy address/version data from the debug=1 goroutine dump and updates the `waitgroup.stuck` span event to only attach `ovm.stuck.goroutineProfile` for the goroutine that performed the capture (others emit counts only). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit eb63ad458cbed03a1693833da0ae5868184f0be4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 8072fdcf0acc515f158bc8e61e5d15311ce27256 --- go/discovery/enginerequests.go | 64 ++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/go/discovery/enginerequests.go b/go/discovery/enginerequests.go index 6592e8af..0cbce5ee 100644 --- a/go/discovery/enginerequests.go +++ b/go/discovery/enginerequests.go @@ -5,8 +5,10 @@ import ( "context" "errors" "fmt" + "regexp" "runtime" "runtime/pprof" + "strings" "sync" "sync/atomic" "time" @@ -19,6 +21,7 @@ import ( "github.com/sourcegraph/conc/pool" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" + "golang.org/x/sync/singleflight" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -157,20 +160,48 @@ func (e *Engine) HandleQuery(ctx context.Context, query *sdp.Query) { } } -// captureGoroutineSummary returns an aggregated goroutine profile (pprof -// debug=1 format) truncated to maxBytes. The debug=1 format groups goroutines -// by unique stack trace, keeping output compact enough for a Honeycomb string -// attribute (49 KB limit). -func captureGoroutineSummary(maxBytes int) string { - var buf bytes.Buffer - _ = pprof.Lookup("goroutine").WriteTo(&buf, 1) - s := buf.String() - if len(s) > maxBytes { - s = s[:maxBytes-20] + "\n...[truncated]..." - } +var ( + goroutineProfileGroup singleflight.Group + + // Compiled once; used by compactGoroutineProfile to strip noise from + // pprof debug=1 output while keeping it human-readable. + profileAddrList = regexp.MustCompile(` @ (?:0x[0-9a-f]+ ?)+`) + profileHexAddr = regexp.MustCompile(`#\t0x[0-9a-f]+\t`) + profileFuncOffset = regexp.MustCompile(`\+0x[0-9a-f]+`) + profileVersion = regexp.MustCompile(`@v[0-9]+\.[0-9]+\.[0-9]+[-\w.]*`) +) + +// compactGoroutineProfile removes noise from a pprof debug=1 goroutine dump +// without losing readability. Typical compression is ~50%, effectively doubling +// how much fits in the Honeycomb 49 KB string attribute limit. +func compactGoroutineProfile(s string) string { + s = strings.ReplaceAll(s, "github.com/overmindtech/workspace/", "g.c/o/w/") + s = strings.ReplaceAll(s, "github.com/", "g.c/") + s = profileAddrList.ReplaceAllString(s, "") // "32257 @ 0x9484c ..." → "32257" + s = profileHexAddr.ReplaceAllString(s, "#\t") // "#\t0xaeda7b\tfoo" → "#\tfoo" + s = profileFuncOffset.ReplaceAllString(s, "") // "Execute+0x4cb" → "Execute" + s = profileVersion.ReplaceAllString(s, "") // "@v1.49.0" → "" return s } +// captureGoroutineSummary returns a compacted goroutine profile (pprof debug=1) +// truncated to maxBytes, deduplicated via singleflight. When many ExecuteQuery +// goroutines hit the stuck timeout simultaneously, only one runs the +// (stop-the-world) pprof capture; the rest share its result. The shared return +// value indicates whether this caller reused another's capture. +func captureGoroutineSummary(maxBytes int) (profile string, shared bool) { + v, _, shared := goroutineProfileGroup.Do("goroutine-profile", func() (any, error) { + var buf bytes.Buffer + _ = pprof.Lookup("goroutine").WriteTo(&buf, 1) + s := compactGoroutineProfile(buf.String()) + if len(s) > maxBytes { + s = s[:maxBytes-20] + "\n...[truncated]..." + } + return s, nil + }) + return v.(string), shared +} + var listExecutionPoolCount atomic.Int32 var getExecutionPoolCount atomic.Int32 @@ -311,14 +342,17 @@ func (e *Engine) ExecuteQuery(ctx context.Context, query *sdp.Query, responses c return case <-time.After(longRunningAdaptersTimeout): // If we're here, then the wait group didn't finish in time - goroutineSummary := captureGoroutineSummary(48_000) + goroutineSummary, profileShared := captureGoroutineSummary(48_000) expandedMutex.RLock() - span.AddEvent("waitgroup.stuck", trace.WithAttributes( + stuckAttrs := []attribute.KeyValue{ attribute.Int("ovm.stuck.goroutineCount", runtime.NumGoroutine()), - attribute.String("ovm.stuck.goroutineProfile", goroutineSummary), attribute.Int("ovm.stuck.totalQueries", totalQueries), attribute.Int("ovm.stuck.remainingQueries", len(expanded)), - )) + } + if !profileShared { + stuckAttrs = append(stuckAttrs, attribute.String("ovm.stuck.goroutineProfile", goroutineSummary)) + } + span.AddEvent("waitgroup.stuck", trace.WithAttributes(stuckAttrs...)) for q, adapter := range expanded { span.AddEvent("waitgroup.stuck.adapter", trace.WithAttributes( attribute.String("ovm.stuck.adapter", adapter.Name()), From c214001f733e0da67e65af1439383605dd00c343 Mon Sep 17 00:00:00 2001 From: TP Honey Date: Thu, 5 Mar 2026 07:47:37 +0000 Subject: [PATCH 59/74] [ENG-2960] Fix K8s Endpoints/EndpointSlice adapter links and backward compatibility (#4113) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Fix the silent bug where the Service adapter linked to type `"Endpoint"` (singular, matches no adapter) instead of `"Endpoints"` (plural) - Add bidirectional Service ↔ EndpointSlice links so the infrastructure graph covers both legacy and modern K8s clusters - Replace the `TODO: migrate` nolint comment with a block comment explaining the backward compatibility strategy for the deprecated `v1.Endpoints` API ## Linear Ticket - **Ticket**: [ENG-2960](https://linear.app/overmind/issue/ENG-2960) — Plan: Fix K8s Endpoints/EndpointSlice adapter links and backward compatibility - **Purpose**: Restore the broken Service → Endpoints link, add EndpointSlice links in both directions, and document why the Endpoints adapter is retained - **Parent**: ENG-2949 ## Changes **`k8s-source/adapters/endpoints.go`** — Replaced the `//nolint:staticcheck // TODO: migrate` directive with a block comment explaining the backward compatibility rationale and a shorter `//nolint:staticcheck // See note at top of file`. **`k8s-source/adapters/service.go`** — Fixed the linked item type from `"Endpoint"` to `"Endpoints"` (GET). Added a new `"EndpointSlice"` SEARCH link using `kubernetes.io/service-name` label selector. Updated `PotentialLinks`. **`k8s-source/adapters/endpointslice.go`** — Added a Service GET link by reading the `kubernetes.io/service-name` label. Added `"Service"` to `PotentialLinks`. **`k8s-source/adapters/service_test.go`** / **`endpointslice_test.go`** — Updated test expectations to match the new links. **Documentation** (`Service.json`, `EndpointSlice.json`, `Service.md`, `EndpointSlice.md`) — Updated `potentialLinks` and added link description sections. **Frontend mocks** (`listActiveSourcesStatus.ts`) — Fixed Service mock `potentialLinks` and added `"Service"` to EndpointSlice mocks. ## Deviations from Approved Plan Implementation matches the approved plan — no material deviations. --- > [!NOTE] > **Low Risk** > Low risk: primarily fixes link metadata/type mismatches and adds additional linked-item queries, with no changes to core data processing beyond relationship discovery. > > **Overview** > Fixes a broken Kubernetes relationship by changing the Service adapter to link to `Endpoints` (plural) instead of the non-existent `Endpoint` type. > > Adds **bidirectional** links between `Service` and `EndpointSlice`: Services now search for matching EndpointSlices via the `kubernetes.io/service-name` label selector, and EndpointSlices link back to their parent Service via the same label. > > Updates adapter `PotentialLinks`, unit tests, docs (`Service.md`, `EndpointSlice.md`, and JSON metadata), and frontend source-status mocks to reflect the corrected and expanded link graph, plus clarifies why the deprecated `core/v1.Endpoints` adapter is intentionally retained for backward compatibility. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 14508d01992fc84e8e41c52c362d074422fd0445. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: fd1d88c406097bcbd4eed02c7d7c626b3f57d496 --- .../docs/sources/k8s/Types/EndpointSlice.md | 4 ++++ .../docs/sources/k8s/Types/Service.md | 8 ++++++++ .../docs/sources/k8s/data/EndpointSlice.json | 2 +- .../docs/sources/k8s/data/Service.json | 2 +- k8s-source/adapters/endpoints.go | 11 ++++++++++- k8s-source/adapters/endpointslice.go | 13 ++++++++++++- k8s-source/adapters/endpointslice_test.go | 6 ++++++ k8s-source/adapters/service.go | 18 +++++++++++++++--- k8s-source/adapters/service_test.go | 8 +++++++- 9 files changed, 64 insertions(+), 8 deletions(-) diff --git a/docs.overmind.tech/docs/sources/k8s/Types/EndpointSlice.md b/docs.overmind.tech/docs/sources/k8s/Types/EndpointSlice.md index c8322f36..9c9338aa 100644 --- a/docs.overmind.tech/docs/sources/k8s/Types/EndpointSlice.md +++ b/docs.overmind.tech/docs/sources/k8s/Types/EndpointSlice.md @@ -34,3 +34,7 @@ When Kubernetes populates cluster DNS (e.g. `my-service.my-namespace.svc.cluster ### [`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. + +### [`Service`](/sources/k8s/Types/Service) + +Every EndpointSlice carries a `kubernetes.io/service-name` label identifying the Service it belongs to. Overmind reads this label and links the EndpointSlice back to its parent Service, completing the bidirectional relationship in the infrastructure graph. diff --git a/docs.overmind.tech/docs/sources/k8s/Types/Service.md b/docs.overmind.tech/docs/sources/k8s/Types/Service.md index 039fe307..48ad1297 100644 --- a/docs.overmind.tech/docs/sources/k8s/Types/Service.md +++ b/docs.overmind.tech/docs/sources/k8s/Types/Service.md @@ -30,3 +30,11 @@ Each Service is assigned one or more IP addresses (ClusterIP, ExternalIP, LoadBa ### [`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. + +### [`Endpoints`](/sources/k8s/Types/Endpoints) + +Each Service creates a corresponding Endpoints object with the same name that lists the IP addresses of the backing Pods. Overmind links Services to their Endpoints so you can see which addresses are currently active. This uses the legacy `core/v1` API and works on all Kubernetes versions. + +### [`EndpointSlice`](/sources/k8s/Types/EndpointSlice) + +Modern Kubernetes clusters create EndpointSlices (labelled with `kubernetes.io/service-name`) as the scalable replacement for Endpoints. Overmind searches for EndpointSlices matching the Service name so you can trace from a Service to the network endpoints that back it on newer clusters. diff --git a/docs.overmind.tech/docs/sources/k8s/data/EndpointSlice.json b/docs.overmind.tech/docs/sources/k8s/data/EndpointSlice.json index a8a9ef2a..1f41c53e 100644 --- a/docs.overmind.tech/docs/sources/k8s/data/EndpointSlice.json +++ b/docs.overmind.tech/docs/sources/k8s/data/EndpointSlice.json @@ -1,7 +1,7 @@ { "type": "EndpointSlice", "category": 3, - "potentialLinks": ["Node", "Pod", "dns", "ip"], + "potentialLinks": ["Node", "Pod", "dns", "ip", "Service"], "descriptiveName": "Endpoint Slice", "supportedQueryMethods": { "get": true, diff --git a/docs.overmind.tech/docs/sources/k8s/data/Service.json b/docs.overmind.tech/docs/sources/k8s/data/Service.json index 4f22397c..5bc032c9 100644 --- a/docs.overmind.tech/docs/sources/k8s/data/Service.json +++ b/docs.overmind.tech/docs/sources/k8s/data/Service.json @@ -1,7 +1,7 @@ { "type": "Service", "category": 3, - "potentialLinks": ["Pod", "ip", "dns", "Endpoint"], + "potentialLinks": ["Pod", "ip", "dns", "Endpoints", "EndpointSlice"], "descriptiveName": "Service", "supportedQueryMethods": { "get": true, diff --git a/k8s-source/adapters/endpoints.go b/k8s-source/adapters/endpoints.go index 3f132ffe..9fa04764 100644 --- a/k8s-source/adapters/endpoints.go +++ b/k8s-source/adapters/endpoints.go @@ -1,4 +1,13 @@ -//nolint:staticcheck // TODO: migrate from v1.Endpoints to discoveryv1.EndpointSlice +// This adapter uses the deprecated core/v1.Endpoints API intentionally. +// +// We use the latest K8s SDK version but balance that against supporting as many +// Kubernetes versions as possible. Older clusters may not have the +// discoveryv1.EndpointSlice API, so we retain this adapter for backward +// compatibility. The staticcheck lint exceptions below are therefore expected +// and acceptable. When the SDK eventually drops support for v1.Endpoints we +// will need to split out version-specific builds of the k8s-source. + +//nolint:staticcheck // See note at top of file package adapters import ( diff --git a/k8s-source/adapters/endpointslice.go b/k8s-source/adapters/endpointslice.go index b2fc027e..9ac3ffa5 100644 --- a/k8s-source/adapters/endpointslice.go +++ b/k8s-source/adapters/endpointslice.go @@ -20,6 +20,17 @@ func endpointSliceExtractor(resource *v1.EndpointSlice, scope string) ([]*sdp.Li return nil, err } + if serviceName, ok := resource.Labels["kubernetes.io/service-name"]; ok && serviceName != "" { + queries = append(queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "Service", + Method: sdp.QueryMethod_GET, + Query: serviceName, + Scope: scope, + }, + }) + } + for _, endpoint := range resource.Endpoints { if endpoint.Hostname != nil { queries = append(queries, &sdp.LinkedItemQuery{ @@ -102,7 +113,7 @@ var endpointSliceAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ Type: "EndpointSlice", DescriptiveName: "Endpoint Slice", Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, - PotentialLinks: []string{"Node", "Pod", "dns", "ip"}, + PotentialLinks: []string{"Node", "Pod", "dns", "ip", "Service"}, SupportedQueryMethods: DefaultSupportedQueryMethods("EndpointSlice"), TerraformMappings: []*sdp.TerraformMapping{ { diff --git a/k8s-source/adapters/endpointslice_test.go b/k8s-source/adapters/endpointslice_test.go index e1d99776..c9c2e0ed 100644 --- a/k8s-source/adapters/endpointslice_test.go +++ b/k8s-source/adapters/endpointslice_test.go @@ -59,6 +59,12 @@ func TestEndpointSliceAdapter(t *testing.T) { GetScope: sd.String(), SetupYAML: endpointSliceYAML, GetQueryTests: QueryTests{ + { + ExpectedType: "Service", + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "endpointslice-service", + ExpectedScope: sd.String(), + }, { ExpectedQueryMatches: regexp.MustCompile(`^10\.`), ExpectedType: "ip", diff --git a/k8s-source/adapters/service.go b/k8s-source/adapters/service.go index 6c49ed96..462da57a 100644 --- a/k8s-source/adapters/service.go +++ b/k8s-source/adapters/service.go @@ -60,16 +60,28 @@ func serviceExtractor(resource *v1.Service, scope string) ([]*sdp.LinkedItemQuer }) } - // Services also generate an endpoint with the same name + // Services generate an Endpoints object with the same name (older K8s API) queries = append(queries, &sdp.LinkedItemQuery{ Query: &sdp.Query{ - Type: "Endpoint", + Type: "Endpoints", Method: sdp.QueryMethod_GET, Query: resource.Name, Scope: scope, }, }) + // Modern K8s clusters also create EndpointSlices labelled with the service name + queries = append(queries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "EndpointSlice", + Method: sdp.QueryMethod_SEARCH, + Query: ListOptionsToQuery(&metaV1.ListOptions{ + LabelSelector: "kubernetes.io/service-name=" + resource.Name, + }), + Scope: scope, + }, + }) + for _, ingress := range resource.Status.LoadBalancer.Ingress { if ingress.IP != "" { queries = append(queries, &sdp.LinkedItemQuery{ @@ -124,7 +136,7 @@ var serviceAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ Type: "Service", DescriptiveName: "Service", Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, - PotentialLinks: []string{"Pod", "ip", "dns", "Endpoint"}, + PotentialLinks: []string{"Pod", "ip", "dns", "Endpoints", "EndpointSlice"}, SupportedQueryMethods: DefaultSupportedQueryMethods("Service"), TerraformMappings: []*sdp.TerraformMapping{ { diff --git a/k8s-source/adapters/service_test.go b/k8s-source/adapters/service_test.go index d2fa89e1..a655550f 100644 --- a/k8s-source/adapters/service_test.go +++ b/k8s-source/adapters/service_test.go @@ -66,11 +66,17 @@ func TestServiceAdapter(t *testing.T) { ExpectedQueryMatches: regexp.MustCompile(`app=service-test`), }, { - ExpectedType: "Endpoint", + ExpectedType: "Endpoints", ExpectedMethod: sdp.QueryMethod_GET, ExpectedQuery: "service-test-service", ExpectedScope: sd.String(), }, + { + ExpectedType: "EndpointSlice", + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedScope: sd.String(), + ExpectedQueryMatches: regexp.MustCompile(`kubernetes\.io/service-name=service-test-service`), + }, { ExpectedType: "dns", ExpectedMethod: sdp.QueryMethod_SEARCH, From 2ca5ad013aab4446b7a9bf9f67c80e34bfedd93c Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 5 Mar 2026 10:32:54 +0100 Subject: [PATCH 60/74] perf: replace protojson.Format with proto.Size in publish tracing (#4121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Replace unconditional `protojson.Format(m)` calls with `proto.Size(m)` summary in `Publish`, `PublishRequest`, and `Unmarshal` on the SDP connection hot path - Eliminates expensive JSON serialization and associated allocations that ran on every message even though the output was only consumed at trace log level (disabled in production and dogfood) - Removes the `protojson` import entirely from `connection.go` ## Changes The only file changed is `go/sdp-go/connection.go`. Three call sites that passed `protojson.Format(m)` to `recordMessage` now pass `fmt.Sprintf("%d bytes", proto.Size(m))` instead. The trace logs and span events still record message type (via `reflect.TypeOf`) and subject — only the full JSON body is replaced with a wire-size summary. Two existing TODO comments acknowledging this problem are removed. Made with [Cursor](https://cursor.com) --- > [!NOTE] > **Low Risk** > Low risk performance change that only affects trace/span message payload logging (full JSON replaced with `N bytes`). Main risk is reduced debugging detail in traces. > > **Overview** > **Reduces SDP connection hot-path overhead** by removing `protojson.Format` calls from `Publish`, `PublishRequest`, and `Unmarshal` tracing. > > Trace/span logging now records the protobuf *type* plus a `"%d bytes"` size summary via `proto.Size(m)` instead of the full JSON body, and the `protojson` import/TODOs are removed. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8cb4e40af712cbfea96b29e432dda090af825b19. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 6a3bdd992c6447da693c1925403b5db9180e6e34 --- go/sdp-go/connection.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/go/sdp-go/connection.go b/go/sdp-go/connection.go index f125afc0..5ade42b1 100644 --- a/go/sdp-go/connection.go +++ b/go/sdp-go/connection.go @@ -10,7 +10,6 @@ import ( "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" ) @@ -60,8 +59,7 @@ func recordMessage(ctx context.Context, name, subj, typ, msg string) { } 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)) + recordMessage(ctx, "Publish", subj, fmt.Sprint(reflect.TypeOf(m)), fmt.Sprintf("%d bytes", proto.Size(m))) data, err := proto.Marshal(m) if err != nil { @@ -77,8 +75,7 @@ func (ec *EncodedConnectionImpl) Publish(ctx context.Context, subj string, m pro } 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)) + recordMessage(ctx, "Publish", subj, fmt.Sprint(reflect.TypeOf(m)), fmt.Sprintf("%d bytes", proto.Size(m))) data, err := proto.Marshal(m) if err != nil { @@ -166,7 +163,7 @@ func Unmarshal(ctx context.Context, b []byte, m proto.Message) error { return err } - recordMessage(ctx, "Unmarshal", "unknown", fmt.Sprint(reflect.TypeOf(m)), protojson.Format(m)) + recordMessage(ctx, "Unmarshal", "unknown", fmt.Sprint(reflect.TypeOf(m)), fmt.Sprintf("%d bytes", proto.Size(m))) return nil } From 71e86630923efaf62da51373d44dae3524be8c15 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:54:35 +0000 Subject: [PATCH 61/74] feat: add Azure Public IP Prefixes client and adapter (#4124) image --- > [!NOTE] > **Medium Risk** > Mostly additive, but it extends shared Azure item-type/model enums and the global adapter registration list, which could impact compilation or type lookups across the Azure source if misnamed or conflicting. > > **Overview** > Adds discovery support for Azure `PublicIPPrefix` resources by introducing a `PublicIPPrefixesClient` wrapper and a new `NewNetworkPublicIPPrefix` adapter with `List`, `ListStream`, and `Get` implementations. > > The adapter maps `armnetwork.PublicIPPrefix` into SDP items with health derived from provisioning state and emits linked queries to related resources (e.g., `NetworkPublicIPAddress`, `NetworkNatGateway`, `NetworkLoadBalancer`/frontend config, `NetworkCustomIPPrefix`, `ExtendedLocationCustomLocation`, and `stdlib.NetworkIP`). > > Registers the new adapter in `manual/adapters.go`, adds new shared item types/resources for `CustomIPPrefix` and `ExtendedLocation` custom locations, and includes generated GoMock client + comprehensive unit tests for listing, getting, and link generation. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ccf1451be1e7916b3d1647513efc661b164fe6d1. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 798b18400c4be3588223e0f84f506a49c65e9e01 --- .../clients/public-ip-prefixes-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + .../azure/manual/network-public-ip-prefix.go | 314 ++++++++++++ .../manual/network-public-ip-prefix_test.go | 447 ++++++++++++++++++ sources/azure/shared/item-types.go | 4 + .../mocks/mock_public_ip_prefixes_client.go | 72 +++ sources/azure/shared/models.go | 7 + 7 files changed, 889 insertions(+) create mode 100644 sources/azure/clients/public-ip-prefixes-client.go create mode 100644 sources/azure/manual/network-public-ip-prefix.go create mode 100644 sources/azure/manual/network-public-ip-prefix_test.go create mode 100644 sources/azure/shared/mocks/mock_public_ip_prefixes_client.go diff --git a/sources/azure/clients/public-ip-prefixes-client.go b/sources/azure/clients/public-ip-prefixes-client.go new file mode 100644 index 00000000..092639a2 --- /dev/null +++ b/sources/azure/clients/public-ip-prefixes-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" +) + +//go:generate mockgen -destination=../shared/mocks/mock_public_ip_prefixes_client.go -package=mocks -source=public-ip-prefixes-client.go + +// PublicIPPrefixesPager is a type alias for the generic Pager interface with public IP prefix response type. +type PublicIPPrefixesPager = Pager[armnetwork.PublicIPPrefixesClientListResponse] + +// PublicIPPrefixesClient is an interface for interacting with Azure public IP prefixes. +type PublicIPPrefixesClient interface { + Get(ctx context.Context, resourceGroupName string, publicIPPrefixName string, options *armnetwork.PublicIPPrefixesClientGetOptions) (armnetwork.PublicIPPrefixesClientGetResponse, error) + NewListPager(resourceGroupName string, options *armnetwork.PublicIPPrefixesClientListOptions) PublicIPPrefixesPager +} + +type publicIPPrefixesClient struct { + client *armnetwork.PublicIPPrefixesClient +} + +func (c *publicIPPrefixesClient) Get(ctx context.Context, resourceGroupName string, publicIPPrefixName string, options *armnetwork.PublicIPPrefixesClientGetOptions) (armnetwork.PublicIPPrefixesClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, publicIPPrefixName, options) +} + +func (c *publicIPPrefixesClient) NewListPager(resourceGroupName string, options *armnetwork.PublicIPPrefixesClientListOptions) PublicIPPrefixesPager { + return c.client.NewListPager(resourceGroupName, options) +} + +// NewPublicIPPrefixesClient creates a new PublicIPPrefixesClient from the Azure SDK client. +func NewPublicIPPrefixesClient(client *armnetwork.PublicIPPrefixesClient) PublicIPPrefixesClient { + return &publicIPPrefixesClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 750f1b54..f7ddae40 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -167,6 +167,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create public ip addresses client: %w", err) } + publicIPPrefixesClient, err := armnetwork.NewPublicIPPrefixesClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create public ip prefixes client: %w", err) + } + loadBalancersClient, err := armnetwork.NewLoadBalancersClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create load balancers client: %w", err) @@ -496,6 +501,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewPublicIPAddressesClient(publicIPAddressesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewNetworkPublicIPPrefix( + clients.NewPublicIPPrefixesClient(publicIPPrefixesClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewNetworkLoadBalancer( clients.NewLoadBalancersClient(loadBalancersClient), resourceGroupScopes, @@ -702,6 +711,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewKeyVaultManagedHSMPrivateEndpointConnection(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDBforPostgreSQLDatabase(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkPublicIPAddress(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewNetworkPublicIPPrefix(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkLoadBalancer(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkZone(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkPrivateDNSZone(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/network-public-ip-prefix.go b/sources/azure/manual/network-public-ip-prefix.go new file mode 100644 index 00000000..050ea99c --- /dev/null +++ b/sources/azure/manual/network-public-ip-prefix.go @@ -0,0 +1,314 @@ +package manual + +import ( + "context" + "errors" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "github.com/overmindtech/cli/go/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 NetworkPublicIPPrefixLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkPublicIPPrefix) + +type networkPublicIPPrefixWrapper struct { + client clients.PublicIPPrefixesClient + + *azureshared.MultiResourceGroupBase +} + +func NewNetworkPublicIPPrefix(client clients.PublicIPPrefixesClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &networkPublicIPPrefixWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + azureshared.NetworkPublicIPPrefix, + ), + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/public-ip-prefixes/list +func (n networkPublicIPPrefixWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + pager := n.client.NewListPager(rgScope.ResourceGroup, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + for _, prefix := range page.Value { + if prefix.Name == nil { + continue + } + item, sdpErr := n.azurePublicIPPrefixToSDPItem(prefix, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (n networkPublicIPPrefixWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + pager := n.client.NewListPager(rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + for _, prefix := range page.Value { + if prefix.Name == nil { + continue + } + item, sdpErr := n.azurePublicIPPrefixToSDPItem(prefix, 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/virtualnetwork/public-ip-prefixes/get +func (n networkPublicIPPrefixWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("query must be exactly one part (public IP prefix name)"), scope, n.Type()) + } + publicIPPrefixName := queryParts[0] + if publicIPPrefixName == "" { + return nil, azureshared.QueryError(errors.New("public IP prefix name cannot be empty"), scope, n.Type()) + } + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + resp, err := n.client.Get(ctx, rgScope.ResourceGroup, publicIPPrefixName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + return n.azurePublicIPPrefixToSDPItem(&resp.PublicIPPrefix, scope) +} + +func (n networkPublicIPPrefixWrapper) azurePublicIPPrefixToSDPItem(prefix *armnetwork.PublicIPPrefix, scope string) (*sdp.Item, *sdp.QueryError) { + if prefix.Name == nil { + return nil, azureshared.QueryError(errors.New("public IP prefix name is nil"), scope, n.Type()) + } + + attributes, err := shared.ToAttributesWithExclude(prefix, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.NetworkPublicIPPrefix.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(prefix.Tags), + LinkedItemQueries: []*sdp.LinkedItemQuery{}, + } + + // Link to Custom Location when ExtendedLocation.Name is a custom location resource ID (Microsoft.ExtendedLocation/customLocations) + if prefix.ExtendedLocation != nil && prefix.ExtendedLocation.Name != nil { + customLocationID := *prefix.ExtendedLocation.Name + if strings.Contains(customLocationID, "customLocations") { + customLocationName := azureshared.ExtractResourceName(customLocationID) + if customLocationName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(customLocationID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ExtendedLocationCustomLocation.String(), + Method: sdp.QueryMethod_GET, + Query: customLocationName, + Scope: linkedScope, + }, + }) + } + } + } + + // Link to IP (standard library) for allocated prefix (e.g. "20.10.0.0/28") + if prefix.Properties != nil && prefix.Properties.IPPrefix != nil && *prefix.Properties.IPPrefix != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *prefix.Properties.IPPrefix, + Scope: "global", + }, + }) + } + + if prefix.Properties != nil { + // Link to Custom IP Prefix (parent prefix this prefix is associated with) + if prefix.Properties.CustomIPPrefix != nil && prefix.Properties.CustomIPPrefix.ID != nil { + customPrefixID := *prefix.Properties.CustomIPPrefix.ID + customPrefixName := azureshared.ExtractResourceName(customPrefixID) + if customPrefixName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(customPrefixID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkCustomIPPrefix.String(), + Method: sdp.QueryMethod_GET, + Query: customPrefixName, + Scope: linkedScope, + }, + }) + } + } + + // Link to NAT Gateway + if prefix.Properties.NatGateway != nil && prefix.Properties.NatGateway.ID != nil { + natGatewayID := *prefix.Properties.NatGateway.ID + natGatewayName := azureshared.ExtractResourceName(natGatewayID) + if natGatewayName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(natGatewayID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkNatGateway.String(), + Method: sdp.QueryMethod_GET, + Query: natGatewayName, + Scope: linkedScope, + }, + }) + } + } + + // Link to Load Balancer and Frontend IP Configuration (from frontend IP configuration reference) + if prefix.Properties.LoadBalancerFrontendIPConfiguration != nil && prefix.Properties.LoadBalancerFrontendIPConfiguration.ID != nil { + feConfigID := *prefix.Properties.LoadBalancerFrontendIPConfiguration.ID + // Format: .../loadBalancers/{lbName}/frontendIPConfigurations/{feConfigName} + params := azureshared.ExtractPathParamsFromResourceID(feConfigID, []string{"loadBalancers", "frontendIPConfigurations"}) + if len(params) >= 2 && params[0] != "" && params[1] != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(feConfigID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkLoadBalancer.String(), + Method: sdp.QueryMethod_GET, + Query: params[0], + Scope: linkedScope, + }, + }) + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkLoadBalancerFrontendIPConfiguration.String(), + Method: sdp.QueryMethod_GET, + Query: shared.CompositeLookupKey(params[0], params[1]), + Scope: linkedScope, + }, + }) + } + } + + // Link to each referenced Public IP Address + for _, ref := range prefix.Properties.PublicIPAddresses { + if ref != nil && ref.ID != nil { + refID := *ref.ID + refName := azureshared.ExtractResourceName(refID) + if refName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(refID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPublicIPAddress.String(), + Method: sdp.QueryMethod_GET, + Query: refName, + Scope: linkedScope, + }, + }) + } + } + } + } + + // Health from provisioning state + if prefix.Properties != nil && prefix.Properties.ProvisioningState != nil { + switch *prefix.Properties.ProvisioningState { + case armnetwork.ProvisioningStateSucceeded: + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case armnetwork.ProvisioningStateCreating, armnetwork.ProvisioningStateUpdating, armnetwork.ProvisioningStateDeleting: + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case armnetwork.ProvisioningStateFailed, armnetwork.ProvisioningStateCanceled: + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + return sdpItem, nil +} + +func (n networkPublicIPPrefixWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + NetworkPublicIPPrefixLookupByName, + } +} + +func (n networkPublicIPPrefixWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.NetworkCustomIPPrefix: true, + azureshared.NetworkNatGateway: true, + azureshared.NetworkLoadBalancer: true, + azureshared.NetworkLoadBalancerFrontendIPConfiguration: true, + azureshared.NetworkPublicIPAddress: true, + azureshared.ExtendedLocationCustomLocation: true, + stdlib.NetworkIP: true, + } +} + +func (n networkPublicIPPrefixWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_public_ip_prefix.name", + }, + } +} + +// https://learn.microsoft.com/en-us/azure/role-based-access-control/resource-provider-operations#microsoftnetwork +func (n networkPublicIPPrefixWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Network/publicIPPrefixes/read", + } +} + +func (n networkPublicIPPrefixWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/network-public-ip-prefix_test.go b/sources/azure/manual/network-public-ip-prefix_test.go new file mode 100644 index 00000000..67ad44cf --- /dev/null +++ b/sources/azure/manual/network-public-ip-prefix_test.go @@ -0,0 +1,447 @@ +package manual_test + +import ( + "context" + "errors" + "slices" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "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 TestNetworkPublicIPPrefix(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) { + prefixName := "test-prefix" + prefix := createAzurePublicIPPrefix(prefixName) + + mockClient := mocks.NewMockPublicIPPrefixesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, prefixName, nil).Return( + armnetwork.PublicIPPrefixesClientGetResponse{ + PublicIPPrefix: *prefix, + }, nil) + + wrapper := manual.NewNetworkPublicIPPrefix(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], prefixName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.NetworkPublicIPPrefix.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkPublicIPPrefix.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != prefixName { + t.Errorf("Expected unique attribute value %s, got %s", prefixName, 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) { + // Public IP prefix with no linked resources in base createAzurePublicIPPrefix + queryTests := shared.QueryTests{} + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_WithLinkedResources", func(t *testing.T) { + prefixName := "test-prefix-with-links" + prefix := createAzurePublicIPPrefixWithLinks(prefixName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockPublicIPPrefixesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, prefixName, nil).Return( + armnetwork.PublicIPPrefixesClientGetResponse{ + PublicIPPrefix: *prefix, + }, nil) + + wrapper := manual.NewNetworkPublicIPPrefix(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], prefixName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + scope := subscriptionID + "." + resourceGroup + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.ExtendedLocationCustomLocation.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-custom-location", + ExpectedScope: scope, + }, + { + ExpectedType: stdlib.NetworkIP.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "20.10.0.0/28", + ExpectedScope: "global", + }, + { + ExpectedType: azureshared.NetworkCustomIPPrefix.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-custom-prefix", + ExpectedScope: scope, + }, + { + ExpectedType: azureshared.NetworkNatGateway.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-nat-gateway", + ExpectedScope: scope, + }, + { + ExpectedType: azureshared.NetworkLoadBalancer.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-load-balancer", + ExpectedScope: scope, + }, + { + ExpectedType: azureshared.NetworkLoadBalancerFrontendIPConfiguration.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey("test-load-balancer", "frontend"), + ExpectedScope: scope, + }, + { + ExpectedType: azureshared.NetworkPublicIPAddress.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "referenced-public-ip", + ExpectedScope: scope, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockPublicIPPrefixesClient(ctrl) + + wrapper := manual.NewNetworkPublicIPPrefix(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 public IP prefix name is empty, but got nil") + } + }) + + t.Run("Get_PrefixWithNilName", func(t *testing.T) { + provisioningState := armnetwork.ProvisioningStateSucceeded + prefixWithNilName := &armnetwork.PublicIPPrefix{ + Name: nil, + Location: new("eastus"), + Properties: &armnetwork.PublicIPPrefixPropertiesFormat{ + ProvisioningState: &provisioningState, + }, + } + + mockClient := mocks.NewMockPublicIPPrefixesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "test-prefix", nil).Return( + armnetwork.PublicIPPrefixesClientGetResponse{ + PublicIPPrefix: *prefixWithNilName, + }, nil) + + wrapper := manual.NewNetworkPublicIPPrefix(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-prefix", true) + if qErr == nil { + t.Error("Expected error when public IP prefix has nil name, but got nil") + } + }) + + t.Run("List", func(t *testing.T) { + prefix1 := createAzurePublicIPPrefix("prefix-1") + prefix2 := createAzurePublicIPPrefix("prefix-2") + + mockClient := mocks.NewMockPublicIPPrefixesClient(ctrl) + mockPager := newMockPublicIPPrefixesPager(ctrl, []*armnetwork.PublicIPPrefix{prefix1, prefix2}) + + mockClient.EXPECT().NewListPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkPublicIPPrefix(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.GetType() != azureshared.NetworkPublicIPPrefix.String() { + t.Fatalf("Expected type %s, got: %s", azureshared.NetworkPublicIPPrefix.String(), item.GetType()) + } + } + }) + + t.Run("List_WithNilName", func(t *testing.T) { + prefix1 := createAzurePublicIPPrefix("prefix-1") + provisioningState := armnetwork.ProvisioningStateSucceeded + prefix2NilName := &armnetwork.PublicIPPrefix{ + Name: nil, + Location: new("eastus"), + Tags: map[string]*string{"env": new("test")}, + Properties: &armnetwork.PublicIPPrefixPropertiesFormat{ + ProvisioningState: &provisioningState, + }, + } + + mockClient := mocks.NewMockPublicIPPrefixesClient(ctrl) + mockPager := newMockPublicIPPrefixesPager(ctrl, []*armnetwork.PublicIPPrefix{prefix1, prefix2NilName}) + + mockClient.EXPECT().NewListPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkPublicIPPrefix(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)) + } + if sdpItems[0].UniqueAttributeValue() != "prefix-1" { + t.Errorf("Expected item name 'prefix-1', got: %s", sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("ListStream", func(t *testing.T) { + prefix1 := createAzurePublicIPPrefix("stream-prefix-1") + prefix2 := createAzurePublicIPPrefix("stream-prefix-2") + + mockClient := mocks.NewMockPublicIPPrefixesClient(ctrl) + mockPager := newMockPublicIPPrefixesPager(ctrl, []*armnetwork.PublicIPPrefix{prefix1, prefix2}) + + mockClient.EXPECT().NewListPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkPublicIPPrefix(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)) + } + }) + + t.Run("ErrorHandling", func(t *testing.T) { + expectedErr := errors.New("public IP prefix not found") + + mockClient := mocks.NewMockPublicIPPrefixesClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent-prefix", nil).Return( + armnetwork.PublicIPPrefixesClientGetResponse{}, expectedErr) + + wrapper := manual.NewNetworkPublicIPPrefix(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "nonexistent-prefix", true) + if qErr == nil { + t.Error("Expected error when getting non-existent public IP prefix, but got nil") + } + }) + + t.Run("InterfaceCompliance", func(t *testing.T) { + mockClient := mocks.NewMockPublicIPPrefixesClient(ctrl) + wrapper := manual.NewNetworkPublicIPPrefix(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + w := wrapper.(sources.Wrapper) + + permissions := w.IAMPermissions() + if len(permissions) == 0 { + t.Error("Expected IAMPermissions to return at least one permission") + } + expectedPermission := "Microsoft.Network/publicIPPrefixes/read" + if !slices.Contains(permissions, expectedPermission) { + t.Errorf("Expected IAMPermissions to include %s", expectedPermission) + } + + mappings := w.TerraformMappings() + foundMapping := false + for _, mapping := range mappings { + if mapping.GetTerraformQueryMap() == "azurerm_public_ip_prefix.name" { + foundMapping = true + if mapping.GetTerraformMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected TerraformMethod GET, got: %s", mapping.GetTerraformMethod()) + } + break + } + } + if !foundMapping { + t.Error("Expected TerraformMappings to include 'azurerm_public_ip_prefix.name'") + } + + lookups := w.GetLookups() + foundLookup := false + for _, lookup := range lookups { + if lookup.ItemType == azureshared.NetworkPublicIPPrefix { + foundLookup = true + break + } + } + if !foundLookup { + t.Error("Expected GetLookups to include NetworkPublicIPPrefix") + } + + potentialLinks := w.PotentialLinks() + for _, linkType := range []shared.ItemType{azureshared.ExtendedLocationCustomLocation, azureshared.NetworkCustomIPPrefix, azureshared.NetworkNatGateway, azureshared.NetworkLoadBalancer, azureshared.NetworkLoadBalancerFrontendIPConfiguration, azureshared.NetworkPublicIPAddress, stdlib.NetworkIP} { + if !potentialLinks[linkType] { + t.Errorf("Expected PotentialLinks to include %s", linkType) + } + } + }) +} + +type mockPublicIPPrefixesPager struct { + ctrl *gomock.Controller + items []*armnetwork.PublicIPPrefix + index int + more bool +} + +func newMockPublicIPPrefixesPager(ctrl *gomock.Controller, items []*armnetwork.PublicIPPrefix) clients.PublicIPPrefixesPager { + return &mockPublicIPPrefixesPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockPublicIPPrefixesPager) More() bool { + return m.more +} + +func (m *mockPublicIPPrefixesPager) NextPage(ctx context.Context) (armnetwork.PublicIPPrefixesClientListResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armnetwork.PublicIPPrefixesClientListResponse{ + PublicIPPrefixListResult: armnetwork.PublicIPPrefixListResult{ + Value: []*armnetwork.PublicIPPrefix{}, + }, + }, nil + } + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + return armnetwork.PublicIPPrefixesClientListResponse{ + PublicIPPrefixListResult: armnetwork.PublicIPPrefixListResult{ + Value: []*armnetwork.PublicIPPrefix{item}, + }, + }, nil +} + +func createAzurePublicIPPrefix(name string) *armnetwork.PublicIPPrefix { + provisioningState := armnetwork.ProvisioningStateSucceeded + prefixLength := int32(28) + return &armnetwork.PublicIPPrefix{ + ID: new("/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Network/publicIPPrefixes/" + name), + Name: new(name), + Type: new("Microsoft.Network/publicIPPrefixes"), + Location: new("eastus"), + Tags: map[string]*string{ + "env": new("test"), + "project": new("testing"), + }, + Properties: &armnetwork.PublicIPPrefixPropertiesFormat{ + ProvisioningState: &provisioningState, + PrefixLength: &prefixLength, + }, + } +} + +func createAzurePublicIPPrefixWithLinks(name, subscriptionID, resourceGroup string) *armnetwork.PublicIPPrefix { + prefix := createAzurePublicIPPrefix(name) + prefix.Properties.IPPrefix = new("20.10.0.0/28") + customLocationID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.ExtendedLocation/customLocations/test-custom-location" + customPrefixID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/customIPPrefixes/test-custom-prefix" + natGatewayID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/natGateways/test-nat-gateway" + lbFeConfigID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/loadBalancers/test-load-balancer/frontendIPConfigurations/frontend" + publicIPID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/publicIPAddresses/referenced-public-ip" + + prefix.ExtendedLocation = &armnetwork.ExtendedLocation{ + Name: new(customLocationID), + } + prefix.Properties.CustomIPPrefix = &armnetwork.SubResource{ + ID: new(customPrefixID), + } + prefix.Properties.NatGateway = &armnetwork.NatGateway{ + ID: new(natGatewayID), + } + prefix.Properties.LoadBalancerFrontendIPConfiguration = &armnetwork.SubResource{ + ID: new(lbFeConfigID), + } + prefix.Properties.PublicIPAddresses = []*armnetwork.ReferencedPublicIPAddress{ + {ID: new(publicIPID)}, + } + return prefix +} + +var _ clients.PublicIPPrefixesPager = (*mockPublicIPPrefixesPager)(nil) diff --git a/sources/azure/shared/item-types.go b/sources/azure/shared/item-types.go index 4be4f1ff..a66736a1 100644 --- a/sources/azure/shared/item-types.go +++ b/sources/azure/shared/item-types.go @@ -49,6 +49,7 @@ var ( NetworkLoadBalancerOutboundRule = shared.NewItemType(Azure, Network, LoadBalancerOutboundRule) NetworkLoadBalancerInboundNatPool = shared.NewItemType(Azure, Network, LoadBalancerInboundNatPool) NetworkPublicIPPrefix = shared.NewItemType(Azure, Network, PublicIPPrefix) + NetworkCustomIPPrefix = shared.NewItemType(Azure, Network, CustomIPPrefix) NetworkNatGateway = shared.NewItemType(Azure, Network, NatGateway) NetworkDdosProtectionPlan = shared.NewItemType(Azure, Network, DdosProtectionPlan) NetworkApplicationGateway = shared.NewItemType(Azure, Network, ApplicationGateway) @@ -85,6 +86,9 @@ var ( NetworkServiceEndpointPolicy = shared.NewItemType(Azure, Network, ServiceEndpointPolicy) NetworkIpAllocation = shared.NewItemType(Azure, Network, IpAllocation) + // ExtendedLocation item types + ExtendedLocationCustomLocation = shared.NewItemType(Azure, ExtendedLocation, CustomLocation) + //Storage item types StorageAccount = shared.NewItemType(Azure, Storage, Account) StorageBlobContainer = shared.NewItemType(Azure, Storage, BlobContainer) diff --git a/sources/azure/shared/mocks/mock_public_ip_prefixes_client.go b/sources/azure/shared/mocks/mock_public_ip_prefixes_client.go new file mode 100644 index 00000000..4ee32b09 --- /dev/null +++ b/sources/azure/shared/mocks/mock_public_ip_prefixes_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: public-ip-prefixes-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_public_ip_prefixes_client.go -package=mocks -source=public-ip-prefixes-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockPublicIPPrefixesClient is a mock of PublicIPPrefixesClient interface. +type MockPublicIPPrefixesClient struct { + ctrl *gomock.Controller + recorder *MockPublicIPPrefixesClientMockRecorder + isgomock struct{} +} + +// MockPublicIPPrefixesClientMockRecorder is the mock recorder for MockPublicIPPrefixesClient. +type MockPublicIPPrefixesClientMockRecorder struct { + mock *MockPublicIPPrefixesClient +} + +// NewMockPublicIPPrefixesClient creates a new mock instance. +func NewMockPublicIPPrefixesClient(ctrl *gomock.Controller) *MockPublicIPPrefixesClient { + mock := &MockPublicIPPrefixesClient{ctrl: ctrl} + mock.recorder = &MockPublicIPPrefixesClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockPublicIPPrefixesClient) EXPECT() *MockPublicIPPrefixesClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockPublicIPPrefixesClient) Get(ctx context.Context, resourceGroupName, publicIPPrefixName string, options *armnetwork.PublicIPPrefixesClientGetOptions) (armnetwork.PublicIPPrefixesClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, publicIPPrefixName, options) + ret0, _ := ret[0].(armnetwork.PublicIPPrefixesClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockPublicIPPrefixesClientMockRecorder) Get(ctx, resourceGroupName, publicIPPrefixName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPublicIPPrefixesClient)(nil).Get), ctx, resourceGroupName, publicIPPrefixName, options) +} + +// NewListPager mocks base method. +func (m *MockPublicIPPrefixesClient) NewListPager(resourceGroupName string, options *armnetwork.PublicIPPrefixesClientListOptions) clients.PublicIPPrefixesPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.PublicIPPrefixesPager) + return ret0 +} + +// NewListPager indicates an expected call of NewListPager. +func (mr *MockPublicIPPrefixesClientMockRecorder) NewListPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListPager", reflect.TypeOf((*MockPublicIPPrefixesClient)(nil).NewListPager), resourceGroupName, options) +} diff --git a/sources/azure/shared/models.go b/sources/azure/shared/models.go index 99785ce1..fd34f81f 100644 --- a/sources/azure/shared/models.go +++ b/sources/azure/shared/models.go @@ -49,6 +49,9 @@ const ( // Resources (subscriptions, resource groups) Resources shared.API = "resources" // Microsoft.Resources + + // ExtendedLocation (custom locations, edge zones) + ExtendedLocation shared.API = "extendedlocation" // Microsoft.ExtendedLocation ) // Resources @@ -97,6 +100,7 @@ const ( LoadBalancerOutboundRule shared.Resource = "load-balancer-outbound-rule" LoadBalancerInboundNatPool shared.Resource = "load-balancer-inbound-nat-pool" PublicIPPrefix shared.Resource = "public-ip-prefix" + CustomIPPrefix shared.Resource = "custom-ip-prefix" NatGateway shared.Resource = "nat-gateway" DdosProtectionPlan shared.Resource = "ddos-protection-plan" ApplicationGateway shared.Resource = "application-gateway" @@ -222,4 +226,7 @@ const ( // Authorization resources RoleAssignment shared.Resource = "role-assignment" RoleDefinition shared.Resource = "role-definition" + + // ExtendedLocation resources + CustomLocation shared.Resource = "custom-location" ) From 369f7375050cc09e75417684a58cc6754e9754f6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 14:07:31 +0000 Subject: [PATCH 62/74] chore(deps): update github actions (major) (#4139) 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 | |---|---|---|---| | [actions/cache](https://redirect.github.com/actions/cache) | action | major | `v4` → `v5` | | [actions/checkout](https://redirect.github.com/actions/checkout) | action | major | `v4` → `v6` | | [actions/upload-artifact](https://redirect.github.com/actions/upload-artifact) | action | major | `v6` → `v7` | | [actions/upload-artifact](https://redirect.github.com/actions/upload-artifact) | action | major | `v4` → `v7` | | [aws-actions/configure-aws-credentials](https://redirect.github.com/aws-actions/configure-aws-credentials) | action | major | `v5` → `v6` | | [crazy-max/ghaction-import-gpg](https://redirect.github.com/crazy-max/ghaction-import-gpg) | action | major | `v6` → `v7` | | [dawidd6/action-download-artifact](https://redirect.github.com/dawidd6/action-download-artifact) | action | major | `v12` → `v16` | | [docker/login-action](https://redirect.github.com/docker/login-action) | action | major | `v3` → `v4` | | [goreleaser/goreleaser-action](https://redirect.github.com/goreleaser/goreleaser-action) | action | major | `v6` → `v7` | | [hashicorp/setup-terraform](https://redirect.github.com/hashicorp/setup-terraform) | action | major | `v3` → `v4` | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/370) for more information. --- ### Release Notes
actions/cache (actions/cache) ### [`v5`](https://redirect.github.com/actions/cache/compare/v4...v5) [Compare Source](https://redirect.github.com/actions/cache/compare/v4...v5)
actions/checkout (actions/checkout) ### [`v6`](https://redirect.github.com/actions/checkout/compare/v5...v6) [Compare Source](https://redirect.github.com/actions/checkout/compare/v5...v6) ### [`v5`](https://redirect.github.com/actions/checkout/compare/v4...v5) [Compare Source](https://redirect.github.com/actions/checkout/compare/v4...v5)
actions/upload-artifact (actions/upload-artifact) ### [`v7`](https://redirect.github.com/actions/upload-artifact/compare/v6...v7) [Compare Source](https://redirect.github.com/actions/upload-artifact/compare/v6...v7)
aws-actions/configure-aws-credentials (aws-actions/configure-aws-credentials) ### [`v6`](https://redirect.github.com/aws-actions/configure-aws-credentials/compare/v5...v6) [Compare Source](https://redirect.github.com/aws-actions/configure-aws-credentials/compare/v5...v6)
crazy-max/ghaction-import-gpg (crazy-max/ghaction-import-gpg) ### [`v7`](https://redirect.github.com/crazy-max/ghaction-import-gpg/compare/v6...v7) [Compare Source](https://redirect.github.com/crazy-max/ghaction-import-gpg/compare/v6...v7)
dawidd6/action-download-artifact (dawidd6/action-download-artifact) ### [`v16`](https://redirect.github.com/dawidd6/action-download-artifact/releases/tag/v16) [Compare Source](https://redirect.github.com/dawidd6/action-download-artifact/compare/v15...v16) #### What's Changed - build(deps): bump minimatch by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​374](https://redirect.github.com/dawidd6/action-download-artifact/pull/374) - node\_modules: update by [@​dawidd6](https://redirect.github.com/dawidd6) in [#​375](https://redirect.github.com/dawidd6/action-download-artifact/pull/375) **Full Changelog**: ### [`v15`](https://redirect.github.com/dawidd6/action-download-artifact/releases/tag/v15) [Compare Source](https://redirect.github.com/dawidd6/action-download-artifact/compare/v14...v15) #### What's Changed - build(deps): bump [@​actions/artifact](https://redirect.github.com/actions/artifact) from 6.0.0 to 6.1.0 by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​369](https://redirect.github.com/dawidd6/action-download-artifact/pull/369) - node\_modules: update by [@​dawidd6](https://redirect.github.com/dawidd6) in [#​370](https://redirect.github.com/dawidd6/action-download-artifact/pull/370) - build(deps): bump fast-xml-parser from 5.3.4 to 5.3.6 by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​371](https://redirect.github.com/dawidd6/action-download-artifact/pull/371) - node\_modules: update by [@​dawidd6](https://redirect.github.com/dawidd6) in [#​372](https://redirect.github.com/dawidd6/action-download-artifact/pull/372) **Full Changelog**: ### [`v14`](https://redirect.github.com/dawidd6/action-download-artifact/releases/tag/v14) [Compare Source](https://redirect.github.com/dawidd6/action-download-artifact/compare/v13...v14) ##### What's Changed - build(deps): bump fast-xml-parser from 5.3.3 to 5.3.4 by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​367](https://redirect.github.com/dawidd6/action-download-artifact/pull/367) - node\_modules: update by [@​dawidd6](https://redirect.github.com/dawidd6) in [#​368](https://redirect.github.com/dawidd6/action-download-artifact/pull/368) **Full Changelog**: ### [`v13`](https://redirect.github.com/dawidd6/action-download-artifact/releases/tag/v13) [Compare Source](https://redirect.github.com/dawidd6/action-download-artifact/compare/v12...v13) #### What's Changed - build(deps): bump [@​actions/artifact](https://redirect.github.com/actions/artifact) from 5.0.1 to 5.0.2 by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​350](https://redirect.github.com/dawidd6/action-download-artifact/pull/350) - build(deps): bump [@​actions/github](https://redirect.github.com/actions/github) from 6.0.1 to 7.0.0 by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​348](https://redirect.github.com/dawidd6/action-download-artifact/pull/348) - build(deps): bump [@​actions/core](https://redirect.github.com/actions/core) from 2.0.1 to 2.0.2 by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​349](https://redirect.github.com/dawidd6/action-download-artifact/pull/349) - node\_modules: update by [@​dawidd6](https://redirect.github.com/dawidd6) in [#​351](https://redirect.github.com/dawidd6/action-download-artifact/pull/351) - build(deps): bump lodash from 4.17.21 to 4.17.23 by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​353](https://redirect.github.com/dawidd6/action-download-artifact/pull/353) - node\_modules: update by [@​dawidd6](https://redirect.github.com/dawidd6) in [#​354](https://redirect.github.com/dawidd6/action-download-artifact/pull/354) - build(deps): bump [@​actions/github](https://redirect.github.com/actions/github) from 7.0.0 to 8.0.0 by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​355](https://redirect.github.com/dawidd6/action-download-artifact/pull/355) - node\_modules: update by [@​dawidd6](https://redirect.github.com/dawidd6) in [#​356](https://redirect.github.com/dawidd6/action-download-artifact/pull/356) - build(deps): bump [@​actions/core](https://redirect.github.com/actions/core) from 2.0.2 to 2.0.3 by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​359](https://redirect.github.com/dawidd6/action-download-artifact/pull/359) - build(deps): bump [@​actions/artifact](https://redirect.github.com/actions/artifact) from 5.0.2 to 6.0.0 by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​361](https://redirect.github.com/dawidd6/action-download-artifact/pull/361) - build(deps): bump [@​actions/core](https://redirect.github.com/actions/core) from 2.0.3 to 3.0.0 by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​360](https://redirect.github.com/dawidd6/action-download-artifact/pull/360) - build(deps): bump [@​actions/github](https://redirect.github.com/actions/github) from 8.0.0 to 9.0.0 by [@​dependabot](https://redirect.github.com/dependabot)\[bot] in [#​357](https://redirect.github.com/dawidd6/action-download-artifact/pull/357) - Convert from CommonJS to ESM by [@​Copilot](https://redirect.github.com/Copilot) in [#​362](https://redirect.github.com/dawidd6/action-download-artifact/pull/362) - Fix ES module imports for [@​actions](https://redirect.github.com/actions) packages by [@​Copilot](https://redirect.github.com/Copilot) in [#​365](https://redirect.github.com/dawidd6/action-download-artifact/pull/365) - node\_modules: update by [@​dawidd6](https://redirect.github.com/dawidd6) in [#​366](https://redirect.github.com/dawidd6/action-download-artifact/pull/366) #### New Contributors - [@​Copilot](https://redirect.github.com/Copilot) made their first contribution in [#​362](https://redirect.github.com/dawidd6/action-download-artifact/pull/362) **Full Changelog**:
docker/login-action (docker/login-action) ### [`v4`](https://redirect.github.com/docker/login-action/compare/v3...v4) [Compare Source](https://redirect.github.com/docker/login-action/compare/v3...v4)
goreleaser/goreleaser-action (goreleaser/goreleaser-action) ### [`v7`](https://redirect.github.com/goreleaser/goreleaser-action/compare/v6...v7) [Compare Source](https://redirect.github.com/goreleaser/goreleaser-action/compare/v6...v7)
hashicorp/setup-terraform (hashicorp/setup-terraform) ### [`v4`](https://redirect.github.com/hashicorp/setup-terraform/compare/v3...v4) [Compare Source](https://redirect.github.com/hashicorp/setup-terraform/compare/v3...v4)
--- ### 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. 👻 **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: bb72c8548df9e4f118d526ca829620e5b6bac39d --- .github/workflows/docker-release.yml | 2 +- .github/workflows/release.yml | 2 +- aws-source/module/provider/.github/workflows/release.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index d83d75c8..b69aaba9 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -29,7 +29,7 @@ jobs: echo "Version: $VERSION" - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04b7f572..4050aad0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,7 +50,7 @@ jobs: cache: true - name: Run GoReleaser (publish) - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@v7 with: version: latest args: release --clean diff --git a/aws-source/module/provider/.github/workflows/release.yml b/aws-source/module/provider/.github/workflows/release.yml index 130f5746..ae76b5a4 100644 --- a/aws-source/module/provider/.github/workflows/release.yml +++ b/aws-source/module/provider/.github/workflows/release.yml @@ -31,7 +31,7 @@ jobs: GPG_FINGERPRINT: 'op://global/Terraform Provider GPG Key/fingerprint' - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v6 + uses: crazy-max/ghaction-import-gpg@v7 id: import_gpg with: gpg_private_key: ${{ env.GPG_PRIVATE_KEY }} @@ -43,7 +43,7 @@ jobs: go-version-file: go.mod - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@v7 with: version: latest args: release --clean From ce1ee72252dd789865d57c706d6d8de7d0d15385 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Thu, 5 Mar 2026 16:29:40 +0000 Subject: [PATCH 63/74] Eng 2886 create networkddosprotectionplan adapter (#4129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit image --- > [!NOTE] > **Medium Risk** > Moderate risk: introduces new Azure Network API calls and registers an additional adapter in the discovery pipeline, which could affect discovery performance/permissions but doesn’t alter existing resource logic. > > **Overview** > Adds first-class discovery for Azure **DDoS protection plans** via a new `NewNetworkDdosProtectionPlan` wrapper supporting `List`, `ListStream`, and `Get`, mapping plans into SDP items with tags excluded from attributes, health derived from provisioning state, and links to associated VNets and public IPs. > > Wires the new adapter into `manual/adapters.go` by creating an `armnetwork.DdosProtectionPlansClient` during initialization (and adding a placeholder adapter for metadata-only mode), and introduces a small `clients.DdosProtectionPlansClient` wrapper interface plus generated GoMock and unit tests for the new adapter. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit b47d21a469cf1d384ee14cd829ec3ebdab4d8a8d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: fe6ea9db02d8e9d9e7ea2dde7527e1f3ca3d619a --- .../clients/ddos-protection-plans-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + .../manual/network-ddos-protection-plan.go | 225 ++++++++++ .../network-ddos-protection-plan_test.go | 397 ++++++++++++++++++ .../mock_ddos_protection_plans_client.go | 72 ++++ 5 files changed, 739 insertions(+) create mode 100644 sources/azure/clients/ddos-protection-plans-client.go create mode 100644 sources/azure/manual/network-ddos-protection-plan.go create mode 100644 sources/azure/manual/network-ddos-protection-plan_test.go create mode 100644 sources/azure/shared/mocks/mock_ddos_protection_plans_client.go diff --git a/sources/azure/clients/ddos-protection-plans-client.go b/sources/azure/clients/ddos-protection-plans-client.go new file mode 100644 index 00000000..426ef05c --- /dev/null +++ b/sources/azure/clients/ddos-protection-plans-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" +) + +//go:generate mockgen -destination=../shared/mocks/mock_ddos_protection_plans_client.go -package=mocks -source=ddos-protection-plans-client.go + +// DdosProtectionPlansPager is a type alias for the generic Pager interface with DDoS protection plan list response type. +type DdosProtectionPlansPager = Pager[armnetwork.DdosProtectionPlansClientListByResourceGroupResponse] + +// DdosProtectionPlansClient is an interface for interacting with Azure DDoS protection plans. +type DdosProtectionPlansClient interface { + Get(ctx context.Context, resourceGroupName string, ddosProtectionPlanName string, options *armnetwork.DdosProtectionPlansClientGetOptions) (armnetwork.DdosProtectionPlansClientGetResponse, error) + NewListByResourceGroupPager(resourceGroupName string, options *armnetwork.DdosProtectionPlansClientListByResourceGroupOptions) DdosProtectionPlansPager +} + +type ddosProtectionPlansClient struct { + client *armnetwork.DdosProtectionPlansClient +} + +func (c *ddosProtectionPlansClient) Get(ctx context.Context, resourceGroupName string, ddosProtectionPlanName string, options *armnetwork.DdosProtectionPlansClientGetOptions) (armnetwork.DdosProtectionPlansClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, ddosProtectionPlanName, options) +} + +func (c *ddosProtectionPlansClient) NewListByResourceGroupPager(resourceGroupName string, options *armnetwork.DdosProtectionPlansClientListByResourceGroupOptions) DdosProtectionPlansPager { + return c.client.NewListByResourceGroupPager(resourceGroupName, options) +} + +// NewDdosProtectionPlansClient creates a new DdosProtectionPlansClient from the Azure SDK client. +func NewDdosProtectionPlansClient(client *armnetwork.DdosProtectionPlansClient) DdosProtectionPlansClient { + return &ddosProtectionPlansClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index f7ddae40..64361fa6 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -172,6 +172,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create public ip prefixes client: %w", err) } + ddosProtectionPlansClient, err := armnetwork.NewDdosProtectionPlansClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create DDoS protection plans client: %w", err) + } + loadBalancersClient, err := armnetwork.NewLoadBalancersClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create load balancers client: %w", err) @@ -505,6 +510,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewPublicIPPrefixesClient(publicIPPrefixesClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewNetworkDdosProtectionPlan( + clients.NewDdosProtectionPlansClient(ddosProtectionPlansClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewNetworkLoadBalancer( clients.NewLoadBalancersClient(loadBalancersClient), resourceGroupScopes, @@ -712,6 +721,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewDBforPostgreSQLDatabase(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkPublicIPAddress(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkPublicIPPrefix(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewNetworkDdosProtectionPlan(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkLoadBalancer(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkZone(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkPrivateDNSZone(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/network-ddos-protection-plan.go b/sources/azure/manual/network-ddos-protection-plan.go new file mode 100644 index 00000000..0e65fd1a --- /dev/null +++ b/sources/azure/manual/network-ddos-protection-plan.go @@ -0,0 +1,225 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +var NetworkDdosProtectionPlanLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkDdosProtectionPlan) + +type networkDdosProtectionPlanWrapper struct { + client clients.DdosProtectionPlansClient + + *azureshared.MultiResourceGroupBase +} + +func NewNetworkDdosProtectionPlan(client clients.DdosProtectionPlansClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &networkDdosProtectionPlanWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + azureshared.NetworkDdosProtectionPlan, + ), + } +} + +// ref: https://learn.microsoft.com/en-us/rest/api/virtualnetwork/ddos-protection-plans/list-by-resource-group +func (n networkDdosProtectionPlanWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + pager := n.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, n.Type()) + } + for _, plan := range page.Value { + if plan.Name == nil { + continue + } + item, sdpErr := n.azureDdosProtectionPlanToSDPItem(plan, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + return items, nil +} + +func (n networkDdosProtectionPlanWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + pager := n.client.NewListByResourceGroupPager(rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + for _, plan := range page.Value { + if plan.Name == nil { + continue + } + item, sdpErr := n.azureDdosProtectionPlanToSDPItem(plan, 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/virtualnetwork/ddos-protection-plans/get +func (n networkDdosProtectionPlanWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) != 1 { + return nil, azureshared.QueryError(errors.New("query must be exactly one part (DDoS protection plan name)"), scope, n.Type()) + } + planName := queryParts[0] + if planName == "" { + return nil, azureshared.QueryError(errors.New("DDoS protection plan name cannot be empty"), scope, n.Type()) + } + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + resp, err := n.client.Get(ctx, rgScope.ResourceGroup, planName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + return n.azureDdosProtectionPlanToSDPItem(&resp.DdosProtectionPlan, scope) +} + +func (n networkDdosProtectionPlanWrapper) azureDdosProtectionPlanToSDPItem(plan *armnetwork.DdosProtectionPlan, scope string) (*sdp.Item, *sdp.QueryError) { + if plan.Name == nil { + return nil, azureshared.QueryError(errors.New("DDoS protection plan name is nil"), scope, n.Type()) + } + + attributes, err := shared.ToAttributesWithExclude(plan, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.NetworkDdosProtectionPlan.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(plan.Tags), + LinkedItemQueries: []*sdp.LinkedItemQuery{}, + } + + if plan.Properties != nil { + // Link to each associated virtual network + for _, ref := range plan.Properties.VirtualNetworks { + if ref != nil && ref.ID != nil { + vnetID := *ref.ID + vnetName := azureshared.ExtractResourceName(vnetID) + if vnetName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(vnetID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkVirtualNetwork.String(), + Method: sdp.QueryMethod_GET, + Query: vnetName, + Scope: linkedScope, + }, + }) + } + } + } + // Link to each associated public IP address + for _, ref := range plan.Properties.PublicIPAddresses { + if ref != nil && ref.ID != nil { + publicIPID := *ref.ID + publicIPName := azureshared.ExtractResourceName(publicIPID) + if publicIPName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(publicIPID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPublicIPAddress.String(), + Method: sdp.QueryMethod_GET, + Query: publicIPName, + Scope: linkedScope, + }, + }) + } + } + } + } + + // Health from provisioning state + if plan.Properties != nil && plan.Properties.ProvisioningState != nil { + switch *plan.Properties.ProvisioningState { + case armnetwork.ProvisioningStateSucceeded: + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case armnetwork.ProvisioningStateCreating, armnetwork.ProvisioningStateUpdating, armnetwork.ProvisioningStateDeleting: + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case armnetwork.ProvisioningStateFailed, armnetwork.ProvisioningStateCanceled: + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + return sdpItem, nil +} + +func (n networkDdosProtectionPlanWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + NetworkDdosProtectionPlanLookupByName, + } +} + +func (n networkDdosProtectionPlanWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.NetworkVirtualNetwork: true, + azureshared.NetworkPublicIPAddress: true, + } +} + +func (n networkDdosProtectionPlanWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_network_ddos_protection_plan.name", + }, + } +} + +// https://learn.microsoft.com/en-us/azure/role-based-access-control/resource-provider-operations#microsoftnetwork +func (n networkDdosProtectionPlanWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Network/ddosProtectionPlans/read", + } +} + +func (n networkDdosProtectionPlanWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/network-ddos-protection-plan_test.go b/sources/azure/manual/network-ddos-protection-plan_test.go new file mode 100644 index 00000000..f8274bd6 --- /dev/null +++ b/sources/azure/manual/network-ddos-protection-plan_test.go @@ -0,0 +1,397 @@ +package manual_test + +import ( + "context" + "errors" + "slices" + "sync" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "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 TestNetworkDdosProtectionPlan(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) { + planName := "test-ddos-plan" + plan := createAzureDdosProtectionPlan(planName) + + mockClient := mocks.NewMockDdosProtectionPlansClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, planName, nil).Return( + armnetwork.DdosProtectionPlansClientGetResponse{ + DdosProtectionPlan: *plan, + }, nil) + + wrapper := manual.NewNetworkDdosProtectionPlan(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], planName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.NetworkDdosProtectionPlan.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkDdosProtectionPlan.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != planName { + t.Errorf("Expected unique attribute value %s, got %s", planName, 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("Get_WithLinkedResources", func(t *testing.T) { + planName := "test-ddos-plan-with-links" + plan := createAzureDdosProtectionPlanWithLinks(planName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockDdosProtectionPlansClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, planName, nil).Return( + armnetwork.DdosProtectionPlansClientGetResponse{ + DdosProtectionPlan: *plan, + }, nil) + + wrapper := manual.NewNetworkDdosProtectionPlan(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, wrapper.Scopes()[0], planName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + scope := subscriptionID + "." + resourceGroup + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.NetworkVirtualNetwork.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-vnet", + ExpectedScope: scope, + }, + { + ExpectedType: azureshared.NetworkPublicIPAddress.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-public-ip", + ExpectedScope: scope, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockDdosProtectionPlansClient(ctrl) + + wrapper := manual.NewNetworkDdosProtectionPlan(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 DDoS protection plan name is empty, but got nil") + } + }) + + t.Run("Get_PlanWithNilName", func(t *testing.T) { + provisioningState := armnetwork.ProvisioningStateSucceeded + planWithNilName := &armnetwork.DdosProtectionPlan{ + Name: nil, + Location: new("eastus"), + Properties: &armnetwork.DdosProtectionPlanPropertiesFormat{ + ProvisioningState: &provisioningState, + }, + } + + mockClient := mocks.NewMockDdosProtectionPlansClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "test-plan", nil).Return( + armnetwork.DdosProtectionPlansClientGetResponse{ + DdosProtectionPlan: *planWithNilName, + }, nil) + + wrapper := manual.NewNetworkDdosProtectionPlan(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "test-plan", true) + if qErr == nil { + t.Error("Expected error when DDoS protection plan has nil name, but got nil") + } + }) + + t.Run("List", func(t *testing.T) { + plan1 := createAzureDdosProtectionPlan("plan-1") + plan2 := createAzureDdosProtectionPlan("plan-2") + + mockClient := mocks.NewMockDdosProtectionPlansClient(ctrl) + mockPager := newMockDdosProtectionPlansPager(ctrl, []*armnetwork.DdosProtectionPlan{plan1, plan2}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkDdosProtectionPlan(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.GetType() != azureshared.NetworkDdosProtectionPlan.String() { + t.Fatalf("Expected type %s, got: %s", azureshared.NetworkDdosProtectionPlan.String(), item.GetType()) + } + } + }) + + t.Run("List_WithNilName", func(t *testing.T) { + plan1 := createAzureDdosProtectionPlan("plan-1") + provisioningState := armnetwork.ProvisioningStateSucceeded + plan2NilName := &armnetwork.DdosProtectionPlan{ + Name: nil, + Location: new("eastus"), + Tags: map[string]*string{"env": new("test")}, + Properties: &armnetwork.DdosProtectionPlanPropertiesFormat{ + ProvisioningState: &provisioningState, + }, + } + + mockClient := mocks.NewMockDdosProtectionPlansClient(ctrl) + mockPager := newMockDdosProtectionPlansPager(ctrl, []*armnetwork.DdosProtectionPlan{plan1, plan2NilName}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkDdosProtectionPlan(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)) + } + if sdpItems[0].UniqueAttributeValue() != "plan-1" { + t.Errorf("Expected item name 'plan-1', got: %s", sdpItems[0].UniqueAttributeValue()) + } + }) + + t.Run("ListStream", func(t *testing.T) { + plan1 := createAzureDdosProtectionPlan("stream-plan-1") + plan2 := createAzureDdosProtectionPlan("stream-plan-2") + + mockClient := mocks.NewMockDdosProtectionPlansClient(ctrl) + mockPager := newMockDdosProtectionPlansPager(ctrl, []*armnetwork.DdosProtectionPlan{plan1, plan2}) + + mockClient.EXPECT().NewListByResourceGroupPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkDdosProtectionPlan(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)) + } + }) + + t.Run("ErrorHandling", func(t *testing.T) { + expectedErr := errors.New("DDoS protection plan not found") + + mockClient := mocks.NewMockDdosProtectionPlansClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "nonexistent-plan", nil).Return( + armnetwork.DdosProtectionPlansClientGetResponse{}, expectedErr) + + wrapper := manual.NewNetworkDdosProtectionPlan(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, wrapper.Scopes()[0], "nonexistent-plan", true) + if qErr == nil { + t.Error("Expected error when getting non-existent DDoS protection plan, but got nil") + } + }) + + t.Run("InterfaceCompliance", func(t *testing.T) { + mockClient := mocks.NewMockDdosProtectionPlansClient(ctrl) + wrapper := manual.NewNetworkDdosProtectionPlan(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + + w := wrapper.(sources.Wrapper) + + permissions := w.IAMPermissions() + if len(permissions) == 0 { + t.Error("Expected IAMPermissions to return at least one permission") + } + expectedPermission := "Microsoft.Network/ddosProtectionPlans/read" + if !slices.Contains(permissions, expectedPermission) { + t.Errorf("Expected IAMPermissions to include %s", expectedPermission) + } + + mappings := w.TerraformMappings() + foundMapping := false + for _, mapping := range mappings { + if mapping.GetTerraformQueryMap() == "azurerm_network_ddos_protection_plan.name" { + foundMapping = true + if mapping.GetTerraformMethod() != sdp.QueryMethod_GET { + t.Errorf("Expected TerraformMethod GET, got: %s", mapping.GetTerraformMethod()) + } + break + } + } + if !foundMapping { + t.Error("Expected TerraformMappings to include 'azurerm_network_ddos_protection_plan.name'") + } + + lookups := w.GetLookups() + foundLookup := false + for _, lookup := range lookups { + if lookup.ItemType == azureshared.NetworkDdosProtectionPlan { + foundLookup = true + break + } + } + if !foundLookup { + t.Error("Expected GetLookups to include NetworkDdosProtectionPlan") + } + + potentialLinks := w.PotentialLinks() + for _, linkType := range []shared.ItemType{azureshared.NetworkVirtualNetwork, azureshared.NetworkPublicIPAddress} { + if !potentialLinks[linkType] { + t.Errorf("Expected PotentialLinks to include %s", linkType) + } + } + }) +} + +type mockDdosProtectionPlansPager struct { + ctrl *gomock.Controller + items []*armnetwork.DdosProtectionPlan + index int + more bool +} + +func newMockDdosProtectionPlansPager(ctrl *gomock.Controller, items []*armnetwork.DdosProtectionPlan) clients.DdosProtectionPlansPager { + return &mockDdosProtectionPlansPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockDdosProtectionPlansPager) More() bool { + return m.more +} + +func (m *mockDdosProtectionPlansPager) NextPage(ctx context.Context) (armnetwork.DdosProtectionPlansClientListByResourceGroupResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armnetwork.DdosProtectionPlansClientListByResourceGroupResponse{ + DdosProtectionPlanListResult: armnetwork.DdosProtectionPlanListResult{ + Value: []*armnetwork.DdosProtectionPlan{}, + }, + }, nil + } + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + return armnetwork.DdosProtectionPlansClientListByResourceGroupResponse{ + DdosProtectionPlanListResult: armnetwork.DdosProtectionPlanListResult{ + Value: []*armnetwork.DdosProtectionPlan{item}, + }, + }, nil +} + +func createAzureDdosProtectionPlan(name string) *armnetwork.DdosProtectionPlan { + provisioningState := armnetwork.ProvisioningStateSucceeded + return &armnetwork.DdosProtectionPlan{ + ID: new("/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Network/ddosProtectionPlans/" + name), + Name: new(name), + Type: new("Microsoft.Network/ddosProtectionPlans"), + Location: new("eastus"), + Tags: map[string]*string{ + "env": new("test"), + "project": new("testing"), + }, + Properties: &armnetwork.DdosProtectionPlanPropertiesFormat{ + ProvisioningState: &provisioningState, + }, + } +} + +func createAzureDdosProtectionPlanWithLinks(name, subscriptionID, resourceGroup string) *armnetwork.DdosProtectionPlan { + plan := createAzureDdosProtectionPlan(name) + plan.Properties.VirtualNetworks = []*armnetwork.SubResource{ + {ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet")}, + } + plan.Properties.PublicIPAddresses = []*armnetwork.SubResource{ + {ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/publicIPAddresses/test-public-ip")}, + } + return plan +} + +var _ clients.DdosProtectionPlansPager = (*mockDdosProtectionPlansPager)(nil) diff --git a/sources/azure/shared/mocks/mock_ddos_protection_plans_client.go b/sources/azure/shared/mocks/mock_ddos_protection_plans_client.go new file mode 100644 index 00000000..d46a8425 --- /dev/null +++ b/sources/azure/shared/mocks/mock_ddos_protection_plans_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ddos-protection-plans-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_ddos_protection_plans_client.go -package=mocks -source=ddos-protection-plans-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockDdosProtectionPlansClient is a mock of DdosProtectionPlansClient interface. +type MockDdosProtectionPlansClient struct { + ctrl *gomock.Controller + recorder *MockDdosProtectionPlansClientMockRecorder + isgomock struct{} +} + +// MockDdosProtectionPlansClientMockRecorder is the mock recorder for MockDdosProtectionPlansClient. +type MockDdosProtectionPlansClientMockRecorder struct { + mock *MockDdosProtectionPlansClient +} + +// NewMockDdosProtectionPlansClient creates a new mock instance. +func NewMockDdosProtectionPlansClient(ctrl *gomock.Controller) *MockDdosProtectionPlansClient { + mock := &MockDdosProtectionPlansClient{ctrl: ctrl} + mock.recorder = &MockDdosProtectionPlansClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockDdosProtectionPlansClient) EXPECT() *MockDdosProtectionPlansClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockDdosProtectionPlansClient) Get(ctx context.Context, resourceGroupName, ddosProtectionPlanName string, options *armnetwork.DdosProtectionPlansClientGetOptions) (armnetwork.DdosProtectionPlansClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, ddosProtectionPlanName, options) + ret0, _ := ret[0].(armnetwork.DdosProtectionPlansClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockDdosProtectionPlansClientMockRecorder) Get(ctx, resourceGroupName, ddosProtectionPlanName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockDdosProtectionPlansClient)(nil).Get), ctx, resourceGroupName, ddosProtectionPlanName, options) +} + +// NewListByResourceGroupPager mocks base method. +func (m *MockDdosProtectionPlansClient) NewListByResourceGroupPager(resourceGroupName string, options *armnetwork.DdosProtectionPlansClientListByResourceGroupOptions) clients.DdosProtectionPlansPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListByResourceGroupPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.DdosProtectionPlansPager) + return ret0 +} + +// NewListByResourceGroupPager indicates an expected call of NewListByResourceGroupPager. +func (mr *MockDdosProtectionPlansClientMockRecorder) NewListByResourceGroupPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListByResourceGroupPager", reflect.TypeOf((*MockDdosProtectionPlansClient)(nil).NewListByResourceGroupPager), resourceGroupName, options) +} From 8bbb2c7e3d7dcc20ab81a1a7ca32d01e2c1c0b9f Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Thu, 5 Mar 2026 17:59:29 +0000 Subject: [PATCH 64/74] feat: add Azure Virtual Network Gateways client and adapter (#4150) image > [!NOTE] > **Medium Risk** > Adds a new Azure discovery adapter with fairly extensive resource-to-link mapping logic, which could impact graph linking correctness and discovery performance. Changes are additive and scoped to networking resources (no auth or write paths). > > **Overview** > Adds first-class discovery for **Azure Virtual Network Gateways** by introducing a `VirtualNetworkGatewaysClient` wrapper and wiring a new `NewNetworkVirtualNetworkGateway` adapter into Azure adapter initialization (including the placeholder/metadata path). > > The adapter supports `List`/`Get` and enriches gateway items with health plus linked queries to related resources (subnets, public/private IPs, DNS hosts, local network gateways, custom locations, managed identities, VNets, and gateway connections). It also registers new Azure item types/resources for `virtual-network-gateway-connection` and `local-network-gateway`, and includes generated mocks plus unit tests for the new wrapper behavior. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 18d763bcf3688458a6233b14fde93db74a5a3eda. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 1d1f5e8c6be75a7b451cc8cdfd7dc9ffc7fcfe1c --- .../virtual-network-gateways-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + .../manual/network-virtual-network-gateway.go | 488 ++++++++++++++++++ .../network-virtual-network-gateway_test.go | 389 ++++++++++++++ sources/azure/shared/item-types.go | 2 + .../mock_virtual_network_gateways_client.go | 72 +++ sources/azure/shared/models.go | 2 + 7 files changed, 998 insertions(+) create mode 100644 sources/azure/clients/virtual-network-gateways-client.go create mode 100644 sources/azure/manual/network-virtual-network-gateway.go create mode 100644 sources/azure/manual/network-virtual-network-gateway_test.go create mode 100644 sources/azure/shared/mocks/mock_virtual_network_gateways_client.go diff --git a/sources/azure/clients/virtual-network-gateways-client.go b/sources/azure/clients/virtual-network-gateways-client.go new file mode 100644 index 00000000..56401d1b --- /dev/null +++ b/sources/azure/clients/virtual-network-gateways-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" +) + +//go:generate mockgen -destination=../shared/mocks/mock_virtual_network_gateways_client.go -package=mocks -source=virtual-network-gateways-client.go + +// VirtualNetworkGatewaysPager is a type alias for the generic Pager interface with virtual network gateway list response type. +type VirtualNetworkGatewaysPager = Pager[armnetwork.VirtualNetworkGatewaysClientListResponse] + +// VirtualNetworkGatewaysClient is an interface for interacting with Azure virtual network gateways. +type VirtualNetworkGatewaysClient interface { + Get(ctx context.Context, resourceGroupName string, virtualNetworkGatewayName string, options *armnetwork.VirtualNetworkGatewaysClientGetOptions) (armnetwork.VirtualNetworkGatewaysClientGetResponse, error) + NewListPager(resourceGroupName string, options *armnetwork.VirtualNetworkGatewaysClientListOptions) VirtualNetworkGatewaysPager +} + +type virtualNetworkGatewaysClient struct { + client *armnetwork.VirtualNetworkGatewaysClient +} + +func (c *virtualNetworkGatewaysClient) Get(ctx context.Context, resourceGroupName string, virtualNetworkGatewayName string, options *armnetwork.VirtualNetworkGatewaysClientGetOptions) (armnetwork.VirtualNetworkGatewaysClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, virtualNetworkGatewayName, options) +} + +func (c *virtualNetworkGatewaysClient) NewListPager(resourceGroupName string, options *armnetwork.VirtualNetworkGatewaysClientListOptions) VirtualNetworkGatewaysPager { + return c.client.NewListPager(resourceGroupName, options) +} + +// NewVirtualNetworkGatewaysClient creates a new VirtualNetworkGatewaysClient from the Azure SDK client. +func NewVirtualNetworkGatewaysClient(client *armnetwork.VirtualNetworkGatewaysClient) VirtualNetworkGatewaysClient { + return &virtualNetworkGatewaysClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 64361fa6..227d8371 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -246,6 +246,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create application security groups client: %w", err) } + virtualNetworkGatewaysClient, err := armnetwork.NewVirtualNetworkGatewaysClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create virtual network gateways client: %w", err) + } + managedHSMsClient, err := armkeyvault.NewManagedHsmsClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create managed hsms client: %w", err) @@ -582,6 +587,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewApplicationGatewaysClient(applicationGatewaysClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewNetworkVirtualNetworkGateway( + clients.NewVirtualNetworkGatewaysClient(virtualNetworkGatewaysClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewSqlServer( clients.NewSqlServersClient(sqlServersClient), resourceGroupScopes, @@ -737,6 +746,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewNetworkSecurityRule(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkRouteTable(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkApplicationGateway(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewNetworkVirtualNetworkGateway(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewSqlServer(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServer(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServerFirewallRule(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/network-virtual-network-gateway.go b/sources/azure/manual/network-virtual-network-gateway.go new file mode 100644 index 00000000..a08c3785 --- /dev/null +++ b/sources/azure/manual/network-virtual-network-gateway.go @@ -0,0 +1,488 @@ +package manual + +import ( + "context" + "errors" + "net" + "net/url" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "github.com/overmindtech/cli/go/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 NetworkVirtualNetworkGatewayLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkVirtualNetworkGateway) + +type networkVirtualNetworkGatewayWrapper struct { + client clients.VirtualNetworkGatewaysClient + + *azureshared.MultiResourceGroupBase +} + +// NewNetworkVirtualNetworkGateway creates a new networkVirtualNetworkGatewayWrapper instance. +func NewNetworkVirtualNetworkGateway(client clients.VirtualNetworkGatewaysClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &networkVirtualNetworkGatewayWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + azureshared.NetworkVirtualNetworkGateway, + ), + } +} + +func (n networkVirtualNetworkGatewayWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + pager := n.client.NewListPager(rgScope.ResourceGroup, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + for _, gw := range page.Value { + if gw.Name == nil { + continue + } + item, sdpErr := n.azureVirtualNetworkGatewayToSDPItem(gw, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (n networkVirtualNetworkGatewayWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + pager := n.client.NewListPager(rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + + for _, gw := range page.Value { + if gw.Name == nil { + continue + } + item, sdpErr := n.azureVirtualNetworkGatewayToSDPItem(gw, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (n networkVirtualNetworkGatewayWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 1 query part: virtualNetworkGatewayName", + Scope: scope, + ItemType: n.Type(), + } + } + + gatewayName := queryParts[0] + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + resp, err := n.client.Get(ctx, rgScope.ResourceGroup, gatewayName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + return n.azureVirtualNetworkGatewayToSDPItem(&resp.VirtualNetworkGateway, scope) +} + +func (n networkVirtualNetworkGatewayWrapper) azureVirtualNetworkGatewayToSDPItem(gw *armnetwork.VirtualNetworkGateway, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(gw, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + if gw.Name == nil { + return nil, azureshared.QueryError(errors.New("virtual network gateway name is nil"), scope, n.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.NetworkVirtualNetworkGateway.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(gw.Tags), + LinkedItemQueries: []*sdp.LinkedItemQuery{}, + } + + // Health from provisioning state + if gw.Properties != nil && gw.Properties.ProvisioningState != nil { + switch *gw.Properties.ProvisioningState { + case armnetwork.ProvisioningStateSucceeded: + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case armnetwork.ProvisioningStateCreating, armnetwork.ProvisioningStateUpdating, armnetwork.ProvisioningStateDeleting: + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case armnetwork.ProvisioningStateFailed, armnetwork.ProvisioningStateCanceled: + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + // Link from IP configurations: subnet, public IP, private IP + if gw.Properties != nil && gw.Properties.IPConfigurations != nil { + for _, ipConfig := range gw.Properties.IPConfigurations { + if ipConfig == nil || ipConfig.Properties == nil { + continue + } + + // Subnet (SearchableWrapper: virtualNetworks/{vnet}/subnets/{subnet}) + if ipConfig.Properties.Subnet != nil && ipConfig.Properties.Subnet.ID != nil { + subnetID := *ipConfig.Properties.Subnet.ID + params := azureshared.ExtractPathParamsFromResourceID(subnetID, []string{"virtualNetworks", "subnets"}) + if len(params) >= 2 && params[0] != "" && params[1] != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(subnetID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkSubnet.String(), + Method: sdp.QueryMethod_GET, + Scope: linkedScope, + Query: shared.CompositeLookupKey(params[0], params[1]), + }, + }) + } + } + + // Public IP address + if ipConfig.Properties.PublicIPAddress != nil && ipConfig.Properties.PublicIPAddress.ID != nil { + pubIPID := *ipConfig.Properties.PublicIPAddress.ID + pubIPName := azureshared.ExtractResourceName(pubIPID) + if pubIPName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(pubIPID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPublicIPAddress.String(), + Method: sdp.QueryMethod_GET, + Query: pubIPName, + Scope: linkedScope, + }, + }) + } + } + + // Private IP address -> stdlib ip + if ipConfig.Properties.PrivateIPAddress != nil && *ipConfig.Properties.PrivateIPAddress != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *ipConfig.Properties.PrivateIPAddress, + Scope: "global", + }, + }) + } + } + } + + // Inbound DNS forwarding endpoint (read-only IP) + if gw.Properties != nil && gw.Properties.InboundDNSForwardingEndpoint != nil && *gw.Properties.InboundDNSForwardingEndpoint != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *gw.Properties.InboundDNSForwardingEndpoint, + Scope: "global", + }, + }) + } + + // Gateway default site (Local Network Gateway) + if gw.Properties != nil && gw.Properties.GatewayDefaultSite != nil && gw.Properties.GatewayDefaultSite.ID != nil { + localGWID := *gw.Properties.GatewayDefaultSite.ID + localGWName := azureshared.ExtractResourceName(localGWID) + if localGWName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(localGWID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkLocalNetworkGateway.String(), + Method: sdp.QueryMethod_GET, + Query: localGWName, + Scope: linkedScope, + }, + }) + } + } + + // Extended location (custom location) when Name is a custom location resource ID + if gw.ExtendedLocation != nil && gw.ExtendedLocation.Name != nil { + customLocationID := *gw.ExtendedLocation.Name + if strings.Contains(customLocationID, "customLocations") { + customLocationName := azureshared.ExtractResourceName(customLocationID) + if customLocationName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(customLocationID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ExtendedLocationCustomLocation.String(), + Method: sdp.QueryMethod_GET, + Query: customLocationName, + Scope: linkedScope, + }, + }) + } + } + } + + // User-assigned managed identities (map keys are ARM resource IDs) + if gw.Identity != nil && gw.Identity.UserAssignedIdentities != nil { + for identityID := range gw.Identity.UserAssignedIdentities { + if identityID == "" { + continue + } + identityName := azureshared.ExtractResourceName(identityID) + if identityName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(identityID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.ManagedIdentityUserAssignedIdentity.String(), + Method: sdp.QueryMethod_GET, + Query: identityName, + Scope: linkedScope, + }, + }) + } + } + } + + // VNet extended location resource (customer VNet when gateway type is local) + if gw.Properties != nil && gw.Properties.VNetExtendedLocationResourceID != nil && *gw.Properties.VNetExtendedLocationResourceID != "" { + vnetID := *gw.Properties.VNetExtendedLocationResourceID + vnetName := azureshared.ExtractResourceName(vnetID) + if vnetName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(vnetID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkVirtualNetwork.String(), + Method: sdp.QueryMethod_GET, + Query: vnetName, + Scope: linkedScope, + }, + }) + } + } + + // VPN client configuration: RADIUS server address(es) (IP or DNS) + if gw.Properties != nil && gw.Properties.VPNClientConfiguration != nil { + vpnCfg := gw.Properties.VPNClientConfiguration + if vpnCfg.RadiusServerAddress != nil && *vpnCfg.RadiusServerAddress != "" { + appendDNSServerLinkIfValid(&sdpItem.LinkedItemQueries, *vpnCfg.RadiusServerAddress) + } + if vpnCfg.RadiusServers != nil { + for _, radiusServer := range vpnCfg.RadiusServers { + if radiusServer != nil && radiusServer.RadiusServerAddress != nil && *radiusServer.RadiusServerAddress != "" { + appendDNSServerLinkIfValid(&sdpItem.LinkedItemQueries, *radiusServer.RadiusServerAddress) + } + } + } + // AAD authentication URLs (e.g. https://login.microsoftonline.com/{tenant}/) — link DNS hostnames + for _, s := range []*string{vpnCfg.AADTenant, vpnCfg.AADAudience, vpnCfg.AADIssuer} { + if s == nil || *s == "" { + continue + } + host := extractHostFromURLOrHostname(*s) + if host == "" { + continue + } + // Skip if it's an IP address; stdlib ip links are added elsewhere for IPs + if net.ParseIP(host) != nil { + continue + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: host, + Scope: "global", + }, + }) + } + } + + // BGP settings: peering address and IP arrays + if gw.Properties != nil && gw.Properties.BgpSettings != nil { + bgp := gw.Properties.BgpSettings + if bgp.BgpPeeringAddress != nil && *bgp.BgpPeeringAddress != "" { + if net.ParseIP(*bgp.BgpPeeringAddress) != nil { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *bgp.BgpPeeringAddress, + Scope: "global", + }, + }) + } else { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkDNS.String(), + Method: sdp.QueryMethod_SEARCH, + Query: *bgp.BgpPeeringAddress, + Scope: "global", + }, + }) + } + } + if bgp.BgpPeeringAddresses != nil { + for _, peeringAddr := range bgp.BgpPeeringAddresses { + if peeringAddr == nil { + continue + } + for _, ipStr := range peeringAddr.DefaultBgpIPAddresses { + if ipStr != nil && *ipStr != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *ipStr, + Scope: "global", + }, + }) + } + } + for _, ipStr := range peeringAddr.CustomBgpIPAddresses { + if ipStr != nil && *ipStr != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *ipStr, + Scope: "global", + }, + }) + } + } + for _, ipStr := range peeringAddr.TunnelIPAddresses { + if ipStr != nil && *ipStr != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: stdlib.NetworkIP.String(), + Method: sdp.QueryMethod_GET, + Query: *ipStr, + Scope: "global", + }, + }) + } + } + } + } + } + + // Virtual Network Gateway Connections (child resource; list by parent gateway name) + if gw.Name != nil && *gw.Name != "" { + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkVirtualNetworkGatewayConnection.String(), + Method: sdp.QueryMethod_SEARCH, + Scope: scope, + Query: *gw.Name, + }, + }) + } + + return sdpItem, nil +} + +func (n networkVirtualNetworkGatewayWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + NetworkVirtualNetworkGatewayLookupByName, + } +} + +func (n networkVirtualNetworkGatewayWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.NetworkSubnet: true, + azureshared.NetworkPublicIPAddress: true, + azureshared.NetworkLocalNetworkGateway: true, + azureshared.NetworkVirtualNetworkGatewayConnection: true, + azureshared.ExtendedLocationCustomLocation: true, + azureshared.ManagedIdentityUserAssignedIdentity: true, + azureshared.NetworkVirtualNetwork: true, + stdlib.NetworkIP: true, + stdlib.NetworkDNS: true, + } +} + +func (n networkVirtualNetworkGatewayWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_virtual_network_gateway.name", + }, + } +} + +func (n networkVirtualNetworkGatewayWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Network/virtualNetworkGateways/read", + } +} + +func extractHostFromURLOrHostname(s string) string { + s = strings.TrimSpace(s) + if s == "" { + return "" + } + u, err := url.Parse(s) + if err != nil { + return s + } + if u.Host != "" { + return u.Hostname() + } + return s +} + +func (n networkVirtualNetworkGatewayWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/network-virtual-network-gateway_test.go b/sources/azure/manual/network-virtual-network-gateway_test.go new file mode 100644 index 00000000..c0536c76 --- /dev/null +++ b/sources/azure/manual/network-virtual-network-gateway_test.go @@ -0,0 +1,389 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "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/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 TestNetworkVirtualNetworkGateway(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) { + gatewayName := "test-gateway" + gw := createAzureVirtualNetworkGateway(gatewayName) + + mockClient := mocks.NewMockVirtualNetworkGatewaysClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, gatewayName, nil).Return( + armnetwork.VirtualNetworkGatewaysClientGetResponse{ + VirtualNetworkGateway: *gw, + }, nil) + + wrapper := manual.NewNetworkVirtualNetworkGateway(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, gatewayName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.NetworkVirtualNetworkGateway.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkVirtualNetworkGateway.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != gatewayName { + t.Errorf("Expected unique attribute value %s, got %s", gatewayName, 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.NetworkVirtualNetworkGatewayConnection.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: gatewayName, + ExpectedScope: scope, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("Get_WithLinkedResources", func(t *testing.T) { + gatewayName := "test-gateway-with-links" + gw := createAzureVirtualNetworkGatewayWithLinks(gatewayName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockVirtualNetworkGatewaysClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, gatewayName, nil).Return( + armnetwork.VirtualNetworkGatewaysClientGetResponse{ + VirtualNetworkGateway: *gw, + }, nil) + + wrapper := manual.NewNetworkVirtualNetworkGateway(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, gatewayName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.NetworkSubnet.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey("test-vnet", "GatewaySubnet"), + ExpectedScope: scope, + }, + { + ExpectedType: azureshared.NetworkPublicIPAddress.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-gateway-pip", + ExpectedScope: scope, + }, + { + ExpectedType: stdlib.NetworkIP.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "10.0.1.4", + ExpectedScope: "global", + }, + { + ExpectedType: stdlib.NetworkIP.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "10.0.0.5", + ExpectedScope: "global", + }, + { + ExpectedType: azureshared.NetworkVirtualNetworkGatewayConnection.String(), + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: gatewayName, + ExpectedScope: scope, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockVirtualNetworkGatewaysClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "", nil).Return( + armnetwork.VirtualNetworkGatewaysClientGetResponse{}, errors.New("virtual network gateway not found")) + + wrapper := manual.NewNetworkVirtualNetworkGateway(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 gateway with empty name, but got nil") + } + }) + + t.Run("ErrorHandling", func(t *testing.T) { + gatewayName := "nonexistent-gateway" + expectedErr := errors.New("virtual network gateway not found") + + mockClient := mocks.NewMockVirtualNetworkGatewaysClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, gatewayName, nil).Return( + armnetwork.VirtualNetworkGatewaysClientGetResponse{}, expectedErr) + + wrapper := manual.NewNetworkVirtualNetworkGateway(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, gatewayName, true) + if qErr == nil { + t.Fatal("Expected error when gateway not found, got nil") + } + }) + + t.Run("List", func(t *testing.T) { + gw1 := createAzureVirtualNetworkGateway("gateway-1") + gw2 := createAzureVirtualNetworkGateway("gateway-2") + + mockClient := mocks.NewMockVirtualNetworkGatewaysClient(ctrl) + mockPager := newMockVirtualNetworkGatewaysPager(ctrl, []*armnetwork.VirtualNetworkGateway{gw1, gw2}) + + mockClient.EXPECT().NewListPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkVirtualNetworkGateway(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") + } + + items, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(items) != 2 { + t.Fatalf("Expected 2 items, got %d", len(items)) + } + + for i, item := range items { + if item.GetType() != azureshared.NetworkVirtualNetworkGateway.String() { + t.Errorf("Item %d: expected type %s, got %s", i, azureshared.NetworkVirtualNetworkGateway.String(), item.GetType()) + } + if item.Validate() != nil { + t.Errorf("Item %d: validation error: %v", i, item.Validate()) + } + } + }) + + t.Run("ListStream", func(t *testing.T) { + gw1 := createAzureVirtualNetworkGateway("gateway-1") + gw2 := createAzureVirtualNetworkGateway("gateway-2") + + mockClient := mocks.NewMockVirtualNetworkGatewaysClient(ctrl) + mockPager := newMockVirtualNetworkGatewaysPager(ctrl, []*armnetwork.VirtualNetworkGateway{gw1, gw2}) + + mockClient.EXPECT().NewListPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkVirtualNetworkGateway(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listStream, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + var received []*sdp.Item + stream := &collectingStream{items: &received} + listStream.ListStream(ctx, scope, true, stream) + + if len(received) != 2 { + t.Fatalf("Expected 2 items from stream, got %d", len(received)) + } + }) + + t.Run("List_NilNameSkipped", func(t *testing.T) { + gw1 := createAzureVirtualNetworkGateway("gateway-1") + gw2NilName := createAzureVirtualNetworkGateway("gateway-2") + gw2NilName.Name = nil + + mockClient := mocks.NewMockVirtualNetworkGatewaysClient(ctrl) + mockPager := newMockVirtualNetworkGatewaysPager(ctrl, []*armnetwork.VirtualNetworkGateway{gw1, gw2NilName}) + + mockClient.EXPECT().NewListPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkVirtualNetworkGateway(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") + } + + items, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(items) != 1 { + t.Fatalf("Expected 1 item (nil name skipped), got %d", len(items)) + } + if items[0].UniqueAttributeValue() != "gateway-1" { + t.Errorf("Expected only gateway-1, got %s", items[0].UniqueAttributeValue()) + } + }) + + t.Run("GetLookups", func(t *testing.T) { + wrapper := manual.NewNetworkVirtualNetworkGateway(nil, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + lookups := wrapper.GetLookups() + if len(lookups) == 0 { + t.Error("Expected GetLookups to return at least one lookup") + } + found := false + for _, l := range lookups { + if l.ItemType.String() == azureshared.NetworkVirtualNetworkGateway.String() { + found = true + break + } + } + if !found { + t.Error("Expected GetLookups to include NetworkVirtualNetworkGateway") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + wrapper := manual.NewNetworkVirtualNetworkGateway(nil, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + potentialLinks := wrapper.PotentialLinks() + for _, linkType := range []shared.ItemType{ + azureshared.NetworkSubnet, + azureshared.NetworkPublicIPAddress, + azureshared.NetworkLocalNetworkGateway, + azureshared.NetworkVirtualNetworkGatewayConnection, + azureshared.ExtendedLocationCustomLocation, + azureshared.ManagedIdentityUserAssignedIdentity, + azureshared.NetworkVirtualNetwork, + stdlib.NetworkIP, + stdlib.NetworkDNS, + } { + if !potentialLinks[linkType] { + t.Errorf("Expected PotentialLinks to include %s", linkType) + } + } + }) +} + +type collectingStream struct { + items *[]*sdp.Item +} + +func (c *collectingStream) SendItem(item *sdp.Item) { + *c.items = append(*c.items, item) +} + +func (c *collectingStream) SendError(err error) {} + +type mockVirtualNetworkGatewaysPager struct { + ctrl *gomock.Controller + items []*armnetwork.VirtualNetworkGateway + index int + more bool +} + +func newMockVirtualNetworkGatewaysPager(ctrl *gomock.Controller, items []*armnetwork.VirtualNetworkGateway) *mockVirtualNetworkGatewaysPager { + return &mockVirtualNetworkGatewaysPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockVirtualNetworkGatewaysPager) More() bool { + return m.more +} + +func (m *mockVirtualNetworkGatewaysPager) NextPage(ctx context.Context) (armnetwork.VirtualNetworkGatewaysClientListResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armnetwork.VirtualNetworkGatewaysClientListResponse{ + VirtualNetworkGatewayListResult: armnetwork.VirtualNetworkGatewayListResult{ + Value: []*armnetwork.VirtualNetworkGateway{}, + }, + }, nil + } + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + return armnetwork.VirtualNetworkGatewaysClientListResponse{ + VirtualNetworkGatewayListResult: armnetwork.VirtualNetworkGatewayListResult{ + Value: []*armnetwork.VirtualNetworkGateway{item}, + }, + }, nil +} + +func createAzureVirtualNetworkGateway(name string) *armnetwork.VirtualNetworkGateway { + provisioningState := armnetwork.ProvisioningStateSucceeded + gatewayType := armnetwork.VirtualNetworkGatewayTypeVPN + vpnType := armnetwork.VPNTypeRouteBased + return &armnetwork.VirtualNetworkGateway{ + ID: new("/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworkGateways/" + name), + Name: new(name), + Type: new("Microsoft.Network/virtualNetworkGateways"), + Location: new("eastus"), + Tags: map[string]*string{ + "env": new("test"), + "project": new("testing"), + }, + Properties: &armnetwork.VirtualNetworkGatewayPropertiesFormat{ + ProvisioningState: &provisioningState, + GatewayType: &gatewayType, + VPNType: &vpnType, + }, + } +} + +func createAzureVirtualNetworkGatewayWithLinks(name, subscriptionID, resourceGroup string) *armnetwork.VirtualNetworkGateway { + gw := createAzureVirtualNetworkGateway(name) + subnetID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworks/test-vnet/subnets/GatewaySubnet" + publicIPID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/publicIPAddresses/test-gateway-pip" + privateIP := "10.0.1.4" + inboundDNS := "10.0.0.5" + gw.Properties.IPConfigurations = []*armnetwork.VirtualNetworkGatewayIPConfiguration{ + { + ID: new("/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network/virtualNetworkGateways/" + name + "/ipConfigurations/default"), + Name: new("default"), + Properties: &armnetwork.VirtualNetworkGatewayIPConfigurationPropertiesFormat{ + Subnet: &armnetwork.SubResource{ + ID: new(subnetID), + }, + PublicIPAddress: &armnetwork.SubResource{ + ID: new(publicIPID), + }, + PrivateIPAddress: &privateIP, + }, + }, + } + gw.Properties.InboundDNSForwardingEndpoint = &inboundDNS + return gw +} diff --git a/sources/azure/shared/item-types.go b/sources/azure/shared/item-types.go index a66736a1..aae8f3f7 100644 --- a/sources/azure/shared/item-types.go +++ b/sources/azure/shared/item-types.go @@ -74,6 +74,8 @@ var ( NetworkRouteTable = shared.NewItemType(Azure, Network, RouteTable) NetworkRoute = shared.NewItemType(Azure, Network, Route) NetworkVirtualNetworkGateway = shared.NewItemType(Azure, Network, VirtualNetworkGateway) + NetworkVirtualNetworkGatewayConnection = shared.NewItemType(Azure, Network, VirtualNetworkGatewayConnection) + NetworkLocalNetworkGateway = shared.NewItemType(Azure, Network, LocalNetworkGateway) NetworkPrivateDNSZone = shared.NewItemType(Azure, Network, PrivateDNSZone) NetworkZone = shared.NewItemType(Azure, Network, Zone) NetworkDNSRecordSet = shared.NewItemType(Azure, Network, DNSRecordSet) diff --git a/sources/azure/shared/mocks/mock_virtual_network_gateways_client.go b/sources/azure/shared/mocks/mock_virtual_network_gateways_client.go new file mode 100644 index 00000000..4d9a99ef --- /dev/null +++ b/sources/azure/shared/mocks/mock_virtual_network_gateways_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: virtual-network-gateways-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_virtual_network_gateways_client.go -package=mocks -source=virtual-network-gateways-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockVirtualNetworkGatewaysClient is a mock of VirtualNetworkGatewaysClient interface. +type MockVirtualNetworkGatewaysClient struct { + ctrl *gomock.Controller + recorder *MockVirtualNetworkGatewaysClientMockRecorder + isgomock struct{} +} + +// MockVirtualNetworkGatewaysClientMockRecorder is the mock recorder for MockVirtualNetworkGatewaysClient. +type MockVirtualNetworkGatewaysClientMockRecorder struct { + mock *MockVirtualNetworkGatewaysClient +} + +// NewMockVirtualNetworkGatewaysClient creates a new mock instance. +func NewMockVirtualNetworkGatewaysClient(ctrl *gomock.Controller) *MockVirtualNetworkGatewaysClient { + mock := &MockVirtualNetworkGatewaysClient{ctrl: ctrl} + mock.recorder = &MockVirtualNetworkGatewaysClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockVirtualNetworkGatewaysClient) EXPECT() *MockVirtualNetworkGatewaysClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockVirtualNetworkGatewaysClient) Get(ctx context.Context, resourceGroupName, virtualNetworkGatewayName string, options *armnetwork.VirtualNetworkGatewaysClientGetOptions) (armnetwork.VirtualNetworkGatewaysClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, virtualNetworkGatewayName, options) + ret0, _ := ret[0].(armnetwork.VirtualNetworkGatewaysClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockVirtualNetworkGatewaysClientMockRecorder) Get(ctx, resourceGroupName, virtualNetworkGatewayName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockVirtualNetworkGatewaysClient)(nil).Get), ctx, resourceGroupName, virtualNetworkGatewayName, options) +} + +// NewListPager mocks base method. +func (m *MockVirtualNetworkGatewaysClient) NewListPager(resourceGroupName string, options *armnetwork.VirtualNetworkGatewaysClientListOptions) clients.VirtualNetworkGatewaysPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.VirtualNetworkGatewaysPager) + return ret0 +} + +// NewListPager indicates an expected call of NewListPager. +func (mr *MockVirtualNetworkGatewaysClientMockRecorder) NewListPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListPager", reflect.TypeOf((*MockVirtualNetworkGatewaysClient)(nil).NewListPager), resourceGroupName, options) +} diff --git a/sources/azure/shared/models.go b/sources/azure/shared/models.go index fd34f81f..a2a12cb6 100644 --- a/sources/azure/shared/models.go +++ b/sources/azure/shared/models.go @@ -125,6 +125,8 @@ const ( RouteTable shared.Resource = "route-table" Route shared.Resource = "route" VirtualNetworkGateway shared.Resource = "virtual-network-gateway" + VirtualNetworkGatewayConnection shared.Resource = "virtual-network-gateway-connection" + LocalNetworkGateway shared.Resource = "local-network-gateway" PrivateDNSZone shared.Resource = "private-dns-zone" Zone shared.Resource = "zone" DNSRecordSet shared.Resource = "dns-record-set" From 18f0c910ba28bf71bc7630e943d5346d69993219 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Thu, 5 Mar 2026 19:02:16 +0100 Subject: [PATCH 65/74] [ENG-2975] Increase ResponseSender heartbeat interval to 30s with jitter (#4154) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Reduces NATS connection mutex contention by changing the ResponseSender heartbeat interval from 5s to 30s, cutting heartbeat publish rate from ~140/s to ~23/s at 700 concurrent queries - Adds +/-10% uniform random jitter per tick to eliminate the thundering herd pattern where all tickers fire simultaneously - Documents ResponseSender message lifecycle, timing, jitter, and stall detection in `sdp/README.md` ## Linear Ticket - **Ticket**: [ENG-2975](https://linear.app/overmind/issue/ENG-2975) — Increase ResponseSender heartbeat interval to 30s with jitter - **Purpose**: Address NATS backpressure from heartbeat volume identified in "Source WaitGroups stuck" production trigger root cause analysis - **Priority**: High ## Changes **`go/sdp-go/progress.go`** - `DefaultResponseInterval` changed from `5 * time.Second` to `30 * time.Second` - Added `math/rand/v2` import - Replaced `time.NewTicker` loop with `time.After` loop that applies +/-10% uniform random jitter per tick (27s–33s range) - `NextUpdateIn` is computed dynamically as 230% of the interval (69s), so gateway and sdp-js stall detection adapts automatically **`sdp/README.md`** - Added "Heartbeat Behavior and NATS Load Management" section with subsections on timing, jitter, stall detection, and the design rule - Includes a table of all ResponseSender message types (WORKING initial, WORKING heartbeat, COMPLETE, ERROR, CANCELLED) **No changes needed in**: gateway, sdp-js, discovery, or tests (all consume `NextUpdateIn` from the protobuf message, and tests use custom intervals) ## Deviations from Approved Plan The implementation matches the approved plan with one minor addition: - **README section is more comprehensive than planned**: The plan specified documenting heartbeat behavior, timing, jitter, stall detection, and the design rule. The implementation additionally includes a table describing all ResponseSender message types (WORKING, COMPLETE, ERROR, CANCELLED) and their lifecycle — not just the WORKING heartbeats. This provides fuller context for engineers reading the docs and was added during implementation review. All four planned parts (constant change, jitter implementation, test verification, hardcoded timing audit) were implemented exactly as specified. No planned work was omitted or deferred. Made with [Cursor](https://cursor.com) --- > [!NOTE] > **Medium Risk** > Changes responder heartbeat timing from 5s to ~27–33s, which can affect stall/health detection behavior and any components implicitly relying on frequent WORKING updates. The logic is simple but touches core query-progress signaling over NATS, so misconfiguration could delay detection of genuinely stalled responders. > > **Overview** > **ResponseSender heartbeats are now less frequent and de-synchronized to reduce NATS load.** The default `DefaultResponseInterval` is increased from 5s to 30s, and the periodic WORKING publish loop now applies +/-10% uniform random jitter per tick (replacing a fixed `time.NewTicker` cadence). > > Documentation is expanded in `sdp/README.md` to describe the ResponseSender message lifecycle, the new heartbeat timing/jitter behavior, and how `NextUpdateIn`-based stall detection interacts with these settings and NATS connection contention. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9a47505bb89103ae9605d77bec10df75ed1b9f5f. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: fb608bd9b05eab9967f1f8e3ff197b28faf91868 --- go/sdp-go/progress.go | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/go/sdp-go/progress.go b/go/sdp-go/progress.go index f53c5094..d4d8607a 100644 --- a/go/sdp-go/progress.go +++ b/go/sdp-go/progress.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math/rand/v2" "sync" "sync/atomic" "time" @@ -18,8 +19,9 @@ import ( ) // DefaultResponseInterval is the default period of time within which responses -// are sent (5 seconds) -const DefaultResponseInterval = (5 * time.Second) +// are sent (30 seconds). Jitter of +/-10% is applied per tick to prevent a +// thundering herd when many concurrent queries start simultaneously. +const DefaultResponseInterval = (30 * 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 @@ -100,19 +102,24 @@ func (rs *ResponseSender) Start(ctx context.Context, ec EncodedConnection, respo if ec == nil { return } - tick := time.NewTicker(rs.ResponseInterval) - defer tick.Stop() + + // Apply +/-10% uniform random jitter per tick to prevent a thundering + // herd when many ResponseSenders start near-simultaneously. + tenth := rs.ResponseInterval / 10 + base := rs.ResponseInterval - tenth + jitterRange := 2 * tenth for { - var err error + jitter := time.Duration(rand.Int64N(int64(jitterRange))) //nolint:gosec // jitter does not need cryptographic randomness + delay := base + jitter select { case <-rs.monitorKill: return case <-ctx.Done(): return - case <-tick.C: - err = rs.connection.Publish( + case <-time.After(delay): + err := rs.connection.Publish( ctx, rs.ResponseSubject, &QueryResponse{ResponseType: &QueryResponse_Response{Response: &resp}}, From 5c1b60bd181b457703c8fb41f03ef0f9284ec6f7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:13:27 +0000 Subject: [PATCH 66/74] fix(deps): update go (#4163) 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/) | |---|---|---|---| | [cloud.google.com/go/aiplatform](https://redirect.github.com/googleapis/google-cloud-go) | `v1.118.0` → `v1.119.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/cloud.google.com%2fgo%2faiplatform/v1.119.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/cloud.google.com%2fgo%2faiplatform/v1.118.0/v1.119.0?slim=true) | | [cloud.google.com/go/compute](https://redirect.github.com/googleapis/google-cloud-go) | `v1.55.0` → `v1.56.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/cloud.google.com%2fgo%2fcompute/v1.56.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/cloud.google.com%2fgo%2fcompute/v1.55.0/v1.56.0?slim=true) | | [github.com/aws/aws-sdk-go-v2](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.41.2` → `v1.41.3` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2/v1.41.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2/v1.41.2/v1.41.3?slim=true) | | [github.com/aws/aws-sdk-go-v2/config](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.32.10` → `v1.32.11` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fconfig/v1.32.11?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fconfig/v1.32.10/v1.32.11?slim=true) | | [github.com/aws/aws-sdk-go-v2/credentials](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.19.10` → `v1.19.11` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fcredentials/v1.19.11?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fcredentials/v1.19.10/v1.19.11?slim=true) | | [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.18.18` → `v1.18.19` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2ffeature%2fec2%2fimds/v1.18.19?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2ffeature%2fec2%2fimds/v1.18.18/v1.18.19?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/apigateway](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.38.5` → `v1.38.6` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fapigateway/v1.38.6?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fapigateway/v1.38.5/v1.38.6?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/autoscaling](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.64.1` → `v1.64.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fautoscaling/v1.64.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fautoscaling/v1.64.1/v1.64.2?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/cloudfront](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.60.1` → `v1.60.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fcloudfront/v1.60.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fcloudfront/v1.60.1/v1.60.2?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/cloudwatch](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.55.0` → `v1.55.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fcloudwatch/v1.55.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fcloudwatch/v1.55.0/v1.55.1?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/directconnect](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.38.12` → `v1.38.13` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fdirectconnect/v1.38.13?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fdirectconnect/v1.38.12/v1.38.13?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/dynamodb](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.56.0` → `v1.56.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fdynamodb/v1.56.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fdynamodb/v1.56.0/v1.56.1?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/ec2](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.293.0` → `v1.294.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fec2/v1.294.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fec2/v1.293.0/v1.294.0?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/ecs](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.73.0` → `v1.73.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fecs/v1.73.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fecs/v1.73.0/v1.73.1?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/efs](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.41.11` → `v1.41.12` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fefs/v1.41.12?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fefs/v1.41.11/v1.41.12?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/eks](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.80.1` → `v1.80.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2feks/v1.80.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2feks/v1.80.1/v1.80.2?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.33.20` → `v1.33.21` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2felasticloadbalancing/v1.33.21?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2felasticloadbalancing/v1.33.20/v1.33.21?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.54.7` → `v1.54.8` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2felasticloadbalancingv2/v1.54.8?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2felasticloadbalancingv2/v1.54.7/v1.54.8?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/iam](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.53.3` → `v1.53.4` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fiam/v1.53.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fiam/v1.53.3/v1.53.4?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/kms](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.50.1` → `v1.50.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fkms/v1.50.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fkms/v1.50.1/v1.50.2?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/lambda](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.88.1` → `v1.88.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2flambda/v1.88.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2flambda/v1.88.1/v1.88.2?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/networkfirewall](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.59.4` → `v1.59.5` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fnetworkfirewall/v1.59.5?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fnetworkfirewall/v1.59.4/v1.59.5?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/networkmanager](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.41.5` → `v1.41.6` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fnetworkmanager/v1.41.6?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fnetworkmanager/v1.41.5/v1.41.6?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/rds](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.116.1` → `v1.116.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2frds/v1.116.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2frds/v1.116.1/v1.116.2?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/route53](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.62.2` → `v1.62.3` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2froute53/v1.62.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2froute53/v1.62.2/v1.62.3?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/s3](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.96.2` → `v1.96.4` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fs3/v1.96.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fs3/v1.96.2/v1.96.4?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/sesv2](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.59.2` → `v1.59.4` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsesv2/v1.59.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsesv2/v1.59.2/v1.59.4?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/sns](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.39.12` → `v1.39.13` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsns/v1.39.13?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsns/v1.39.12/v1.39.13?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/sqs](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.42.22` → `v1.42.23` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsqs/v1.42.23?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsqs/v1.42.22/v1.42.23?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/ssm](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.68.1` → `v1.68.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fssm/v1.68.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fssm/v1.68.1/v1.68.2?slim=true) | | [github.com/aws/aws-sdk-go-v2/service/sts](https://redirect.github.com/aws/aws-sdk-go-v2) | `v1.41.7` → `v1.41.8` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsts/v1.41.8?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2faws%2faws-sdk-go-v2%2fservice%2fsts/v1.41.7/v1.41.8?slim=true) | | [github.com/brianvoe/gofakeit/v7](https://redirect.github.com/brianvoe/gofakeit) | `v7.14.0` → `v7.14.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fbrianvoe%2fgofakeit%2fv7/v7.14.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fbrianvoe%2fgofakeit%2fv7/v7.14.0/v7.14.1?slim=true) | | [github.com/harness/harness-go-sdk](https://redirect.github.com/harness/harness-go-sdk) | `v0.7.12` → `v0.7.13` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fharness%2fharness-go-sdk/v0.7.13?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fharness%2fharness-go-sdk/v0.7.12/v0.7.13?slim=true) | | [github.com/micahhausler/aws-iam-policy](https://redirect.github.com/micahhausler/aws-iam-policy) | `v0.4.3` → `v0.4.4` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fmicahhausler%2faws-iam-policy/v0.4.4?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fmicahhausler%2faws-iam-policy/v0.4.3/v0.4.4?slim=true) | | [github.com/openai/openai-go/v3](https://redirect.github.com/openai/openai-go) | `v3.24.0` → `v3.26.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fopenai%2fopenai-go%2fv3/v3.26.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fopenai%2fopenai-go%2fv3/v3.24.0/v3.26.0?slim=true) | | [go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp](https://redirect.github.com/open-telemetry/opentelemetry-go-contrib) | `v0.65.0` → `v0.66.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/go.opentelemetry.io%2fcontrib%2finstrumentation%2fnet%2fhttp%2fotelhttp/v0.66.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/go.opentelemetry.io%2fcontrib%2finstrumentation%2fnet%2fhttp%2fotelhttp/v0.65.0/v0.66.0?slim=true) | | [go.opentelemetry.io/otel](https://redirect.github.com/open-telemetry/opentelemetry-go) | `v1.40.0` → `v1.41.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/go.opentelemetry.io%2fotel/v1.41.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/go.opentelemetry.io%2fotel/v1.40.0/v1.41.0?slim=true) | | [go.opentelemetry.io/otel/exporters/otlp/otlptrace](https://redirect.github.com/open-telemetry/opentelemetry-go) | `v1.40.0` → `v1.41.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/go.opentelemetry.io%2fotel%2fexporters%2fotlp%2fotlptrace/v1.41.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/go.opentelemetry.io%2fotel%2fexporters%2fotlp%2fotlptrace/v1.40.0/v1.41.0?slim=true) | | [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp](https://redirect.github.com/open-telemetry/opentelemetry-go) | `v1.40.0` → `v1.41.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/go.opentelemetry.io%2fotel%2fexporters%2fotlp%2fotlptrace%2fotlptracehttp/v1.41.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/go.opentelemetry.io%2fotel%2fexporters%2fotlp%2fotlptrace%2fotlptracehttp/v1.40.0/v1.41.0?slim=true) | | [go.opentelemetry.io/otel/exporters/stdout/stdouttrace](https://redirect.github.com/open-telemetry/opentelemetry-go) | `v1.40.0` → `v1.41.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/go.opentelemetry.io%2fotel%2fexporters%2fstdout%2fstdouttrace/v1.41.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/go.opentelemetry.io%2fotel%2fexporters%2fstdout%2fstdouttrace/v1.40.0/v1.41.0?slim=true) | | [go.opentelemetry.io/otel/sdk](https://redirect.github.com/open-telemetry/opentelemetry-go) | `v1.40.0` → `v1.41.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/go.opentelemetry.io%2fotel%2fsdk/v1.41.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/go.opentelemetry.io%2fotel%2fsdk/v1.40.0/v1.41.0?slim=true) | | [go.opentelemetry.io/otel/trace](https://redirect.github.com/open-telemetry/opentelemetry-go) | `v1.40.0` → `v1.41.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/go.opentelemetry.io%2fotel%2ftrace/v1.41.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/go.opentelemetry.io%2fotel%2ftrace/v1.40.0/v1.41.0?slim=true) | | [google.golang.org/grpc](https://redirect.github.com/grpc/grpc-go) | `v1.79.1` → `v1.79.2` | ![age](https://developer.mend.io/api/mc/badges/age/go/google.golang.org%2fgrpc/v1.79.2?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/google.golang.org%2fgrpc/v1.79.1/v1.79.2?slim=true) | | [sigs.k8s.io/controller-runtime](https://redirect.github.com/kubernetes-sigs/controller-runtime) | `v0.23.1` → `v0.23.3` | ![age](https://developer.mend.io/api/mc/badges/age/go/sigs.k8s.io%2fcontroller-runtime/v0.23.3?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/sigs.k8s.io%2fcontroller-runtime/v0.23.1/v0.23.3?slim=true) | | [sigs.k8s.io/controller-runtime/tools/setup-envtest](https://redirect.github.com/kubernetes-sigs/controller-runtime) | `v0.0.0-20260216173200-e4c1c38bcbdb` → `v0.0.0-20260305141020-105baa6284da` | ![age](https://developer.mend.io/api/mc/badges/age/go/sigs.k8s.io%2fcontroller-runtime%2ftools%2fsetup-envtest/v0.0.0-20260305141020-105baa6284da?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/sigs.k8s.io%2fcontroller-runtime%2ftools%2fsetup-envtest/v0.0.0-20260216173200-e4c1c38bcbdb/v0.0.0-20260305141020-105baa6284da?slim=true) | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/370) for more information. ## ⚠️ Warning These modules are almost certainly going to break everything. They do every time they update. If you update even one repo's OTEL modules, go will then pull in new versions due to [MVS](https://research.swtch.com/vgo-mvs) which will cause your repo to break. All [otel pull requests](https://redirect.github.com/pulls?q=is%3Aopen+is%3Apr+user%3Aovermindtech+archived%3Afalse+label%3Aobservability+) need to be merged basically at the same time, and after all of the modules have been updated to be compatible with each other. --- ### Release Notes
aws/aws-sdk-go-v2 (github.com/aws/aws-sdk-go-v2) ### [`v1.41.3`](https://redirect.github.com/aws/aws-sdk-go-v2/compare/v1.41.2...v1.41.3) [Compare Source](https://redirect.github.com/aws/aws-sdk-go-v2/compare/v1.41.2...v1.41.3)
brianvoe/gofakeit (github.com/brianvoe/gofakeit/v7) ### [`v7.14.1`](https://redirect.github.com/brianvoe/gofakeit/compare/v7.14.0...v7.14.1) [Compare Source](https://redirect.github.com/brianvoe/gofakeit/compare/v7.14.0...v7.14.1)
harness/harness-go-sdk (github.com/harness/harness-go-sdk) ### [`v0.7.13`](https://redirect.github.com/harness/harness-go-sdk/compare/v0.7.12...v0.7.13) [Compare Source](https://redirect.github.com/harness/harness-go-sdk/compare/v0.7.12...v0.7.13)
micahhausler/aws-iam-policy (github.com/micahhausler/aws-iam-policy) ### [`v0.4.4`](https://redirect.github.com/micahhausler/aws-iam-policy/compare/v0.4.3...v0.4.4) [Compare Source](https://redirect.github.com/micahhausler/aws-iam-policy/compare/v0.4.3...v0.4.4)
openai/openai-go (github.com/openai/openai-go/v3) ### [`v3.26.0`](https://redirect.github.com/openai/openai-go/releases/tag/v3.26.0) [Compare Source](https://redirect.github.com/openai/openai-go/compare/v3.25.0...v3.26.0) #### 3.26.0 (2026-03-05) Full Changelog: [v3.25.0...v3.26.0](https://redirect.github.com/openai/openai-\[go/compare/v3.25.0...v3.26.0]\(https://www.golinks.io/compare/v3.25.0...v3.26.0?trackSource=github\)) ##### Features - **api:** The GA ComputerTool now uses the CompuerTool class. The 'computer\_use\_preview' tool is moved to ComputerUsePreview ([347418b](https://redirect.github.com/openai/openai-\[go/commit/347418be8d4fa33881d9ac30f6c7132f2f545f2b]\(https://www.golinks.io/commit/347418be8d4fa33881d9ac30f6c7132f2f545f2b?trackSource=github\))) ### [`v3.25.0`](https://redirect.github.com/openai/openai-go/blob/HEAD/CHANGELOG.md#3250-2026-03-05) [Compare Source](https://redirect.github.com/openai/openai-go/compare/v3.24.0...v3.25.0) Full Changelog: [v3.24.0...v3.25.0](https://redirect.github.com/openai/openai-go/compare/v3.24.0...v3.25.0) ##### Features - **api:** gpt-5.4, tool search tool, and new computer tool ([101826d](https://redirect.github.com/openai/openai-go/commit/101826dd757a0213aecb4eaa6332866657b9aa83)) - **api:** remove Phase from input/output messages, PromptCacheKey from responses ([961b8ca](https://redirect.github.com/openai/openai-go/commit/961b8ca27923beca8aa08d4a8e3382c2da9d61db)) ##### Bug Fixes - **api:** internal schema fixes ([fe5f7cd](https://redirect.github.com/openai/openai-go/commit/fe5f7cdb34d11dd18caa503716cae1512b245053)) - **api:** manual updates ([70b02c8](https://redirect.github.com/openai/openai-go/commit/70b02c8f63c98a17813dc6cb7f7707fb2bba81c5)) - **api:** readd phase ([548aff8](https://redirect.github.com/openai/openai-go/commit/548aff8ad8b96518f5549ec3bc98da71e9b7f540)) ##### Chores - **internal:** codegen related update ([ab733b9](https://redirect.github.com/openai/openai-go/commit/ab733b91db39e99e292696530340333c065e04b9)) - **internal:** codegen related update ([23d1831](https://redirect.github.com/openai/openai-go/commit/23d1831cb5ca6f61ca8575737cec17e2f347818b)) - **internal:** reduce warnings ([2963312](https://redirect.github.com/openai/openai-go/commit/2963312c075fa9a30abad32b1e90813229b22129))
open-telemetry/opentelemetry-go (go.opentelemetry.io/otel) ### [`v1.41.0`](https://redirect.github.com/open-telemetry/opentelemetry-go/releases/tag/v1.41.0): /v0.63.0/v0.17.0/v0.0.15 [Compare Source](https://redirect.github.com/open-telemetry/opentelemetry-go/compare/v1.40.0...v1.41.0) This release is the last to support [Go 1.24]. The next release will require at least [Go 1.25]. ##### Added - Support testing of [Go 1.26]. ([#​7902](https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7902)) ##### Fixed - Update `Baggage` in `go.opentelemetry.io/otel/propagation` and `Parse` and `New` in `go.opentelemetry.io/otel/baggage` to comply with W3C Baggage specification limits. `New` and `Parse` now return partial baggage along with an error when limits are exceeded. Errors from baggage extraction are reported to the global error handler. ([#​7880](https://redirect.github.com/open-telemetry/opentelemetry-go/issues/7880)) [Go 1.26]: https://go.dev/doc/go1.26 [Go 1.25]: https://go.dev/doc/go1.25 [Go 1.24]: https://go.dev/doc/go1.24 #### What's Changed - fix(deps): update googleapis to [`ce8ad4c`](https://redirect.github.com/open-telemetry/opentelemetry-go/commit/ce8ad4c) by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7860](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7860) - chore(deps): update otel/weaver docker tag to v0.21.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7865](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7865) - fix(deps): update module go.opentelemetry.io/collector/pdata to v1.51.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7863](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7863) - chore(deps): update golang.org/x/telemetry digest to [`fe4bb1c`](https://redirect.github.com/open-telemetry/opentelemetry-go/commit/fe4bb1c) by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7861](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7861) - chore(deps): update golang.org/x/telemetry digest to [`aaaaaa5`](https://redirect.github.com/open-telemetry/opentelemetry-go/commit/aaaaaa5) by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7869](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7869) - sdk/log/observ: guard LogProcessed with Enabled by [@​NesterovYehor](https://redirect.github.com/NesterovYehor) in [#​7848](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7848) - stdouttrace observability: skip metric work when instruments are disabled by [@​NesterovYehor](https://redirect.github.com/NesterovYehor) in [#​7853](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7853) - chore(deps): update otel/weaver docker tag to v0.21.2 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7870](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7870) - fix(deps): update googleapis to [`546029d`](https://redirect.github.com/open-telemetry/opentelemetry-go/commit/546029d) by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7871](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7871) - stdoutmetric observ: skip metric work when instruments are disabled by [@​NesterovYehor](https://redirect.github.com/NesterovYehor) in [#​7868](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7868) - chore(deps): update fossas/fossa-action action to v1.8.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7879](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7879) - chore(deps): update github/codeql-action action to v4.32.2 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7878](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7878) - chore(deps): update module github.com/ghostiam/protogetter to v0.3.20 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7877](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7877) - chore(deps): update golang.org/x/telemetry digest to [`86a5c4b`](https://redirect.github.com/open-telemetry/opentelemetry-go/commit/86a5c4b) by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7876](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7876) - fix(deps): update module golang.org/x/sys to v0.41.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7885](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7885) - chore(deps): update module github.com/clipperhouse/uax29/v2 to v2.6.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7884](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7884) - Checked if instrument enabled before measuring in prometheus by [@​itssaharsh](https://redirect.github.com/itssaharsh) in [#​7866](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7866) - exporter/otlploghttp: guard observ metrics with Enabled checks by [@​NesterovYehor](https://redirect.github.com/NesterovYehor) in [#​7813](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7813) - chore(deps): update module github.com/go-git/go-git/v5 to v5.16.5 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7886](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7886) - chore(deps): update golang.org/x by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7887](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7887) - fix(deps): update golang.org/x by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7890](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7890) - fix(deps): update golang.org/x to [`2842357`](https://redirect.github.com/open-telemetry/opentelemetry-go/commit/2842357) by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7891](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7891) - fix(deps): update googleapis to [`4cfbd41`](https://redirect.github.com/open-telemetry/opentelemetry-go/commit/4cfbd41) by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7889](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7889) - Checked if instrument enabled before measuring in `oteltracegrpc` by [@​itssaharsh](https://redirect.github.com/itssaharsh) in [#​7825](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7825) - Checked if Instrument Enabled before measuring in otlpgrpc by [@​itssaharsh](https://redirect.github.com/itssaharsh) in [#​7824](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7824) - chore(deps): update module github.com/grpc-ecosystem/grpc-gateway/v2 to v2.27.8 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7892](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7892) - chore(deps): update module github.com/golangci/golines to v0.15.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7893](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7893) - chore(deps): update module github.com/golangci/misspell to v0.8.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7894](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7894) - chore(deps): update golang.org/x/telemetry digest to [`9f66fae`](https://redirect.github.com/open-telemetry/opentelemetry-go/commit/9f66fae) by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7898](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7898) - fix(deps): update module google.golang.org/grpc to v1.79.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7906](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7906) - Support Go 1.26 by [@​dmathieu](https://redirect.github.com/dmathieu) in [#​7902](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7902) - fix(deps): update module google.golang.org/grpc to v1.79.1 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7908](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7908) - chore(deps): update github/codeql-action action to v4.32.3 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7909](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7909) - chore(deps): update module github.com/kevinburke/ssh\_config to v1.5.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7911](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7911) - chore(deps): update module github.com/kevinburke/ssh\_config to v1.6.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7913](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7913) - chore(deps): update actions/stale action to v10.2.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7917](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7917) - chore(deps): update module github.com/godoc-lint/godoc-lint to v0.11.2 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7916](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7916) - chore(deps): update module github.com/clipperhouse/uax29/v2 to v2.7.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7915](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7915) - chore(deps): update module github.com/mattn/go-runewidth to v0.0.20 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7918](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7918) - chore(deps): update module github.com/grpc-ecosystem/grpc-gateway/v2 to v2.28.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7921](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7921) - Checked if Operation Enabled in `otlptracehttp` before performing operation by [@​itssaharsh](https://redirect.github.com/itssaharsh) in [#​7881](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7881) - chore(deps): update github/codeql-action action to v4.32.4 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7936](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7936) - chore(deps): update module github.com/mirrexone/unqueryvet to v1.5.4 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7939](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7939) - chore(deps): update module github.com/uudashr/gocognit to v1.2.1 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7947](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7947) - chore(deps): update module github.com/alexkohler/prealloc to v1.0.3 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7950](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7950) - chore(deps): update module github.com/go-git/go-billy/v5 to v5.8.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7953](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7953) - chore(deps): update lycheeverse/lychee-action action to v2.8.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7959](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7959) - chore(deps): update module github.com/go-git/go-git/v5 to v5.17.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7960](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7960) - chore(deps): update actions/setup-go action to v6.3.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7962](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7962) - Document metric api interfaces that methods need to be safe to be called concurrently by [@​dashpole](https://redirect.github.com/dashpole) in [#​7952](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7952) - ci: add govulncheck job to CI workflow and update lint target by [@​pellared](https://redirect.github.com/pellared) in [#​7971](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7971) - Comply with W3C Baggage specification limits by [@​XSAM](https://redirect.github.com/XSAM) in [#​7880](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7880) - chore(deps): update module github.com/mgechev/revive to v1.14.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7895](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7895) - chore(deps): update github artifact actions (major) by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7963](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7963) - chore(deps): update module github.com/kisielk/errcheck to v1.10.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7967](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7967) - chore(deps): update module github.com/protonmail/go-crypto to v1.4.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7969](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7969) - fix(deps): update github.com/opentracing-contrib/go-grpc/test digest to [`d566b4d`](https://redirect.github.com/open-telemetry/opentelemetry-go/commit/d566b4d) by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7972](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7972) - chore(deps): update module github.com/sonatard/noctx to v0.5.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7968](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7968) - chore(deps): update module github.com/daixiang0/gci to v0.14.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7973](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7973) - chore(deps): update module github.com/securego/gosec/v2 to v2.23.0 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7899](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7899) - Generate semconv/v1.40.0 by [@​ChrsMark](https://redirect.github.com/ChrsMark) in [#​7929](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7929) - Revert "Generate semconv/v1.40.0" by [@​dmathieu](https://redirect.github.com/dmathieu) in [#​7978](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7978) - chore(deps): update github/codeql-action action to v4.32.5 by [@​renovate](https://redirect.github.com/renovate)\[bot] in [#​7980](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7980) - fix: add error handling for insecure HTTP endpoints with TLS client configuration by [@​sandy2008](https://redirect.github.com/sandy2008) in [#​7914](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7914) - Release 1.41.0/0.63.0/0.17.0/0.0.15 by [@​pellared](https://redirect.github.com/pellared) in [#​7977](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7977) #### New Contributors - [@​NesterovYehor](https://redirect.github.com/NesterovYehor) made their first contribution in [#​7848](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7848) - [@​sandy2008](https://redirect.github.com/sandy2008) made their first contribution in [#​7914](https://redirect.github.com/open-telemetry/opentelemetry-go/pull/7914) **Full Changelog**:
grpc/grpc-go (google.golang.org/grpc) ### [`v1.79.2`](https://redirect.github.com/grpc/grpc-go/releases/tag/v1.79.2): Release 1.79.2 [Compare Source](https://redirect.github.com/grpc/grpc-go/compare/v1.79.1...v1.79.2) ### Bug Fixes - stats: Prevent redundant error logging in health/ORCA producers by skipping stats/tracing processing when no stats handler is configured. ([#​8874](https://redirect.github.com/grpc/grpc-go/pull/8874))
kubernetes-sigs/controller-runtime (sigs.k8s.io/controller-runtime) ### [`v0.23.3`](https://redirect.github.com/kubernetes-sigs/controller-runtime/releases/tag/v0.23.3) [Compare Source](https://redirect.github.com/kubernetes-sigs/controller-runtime/compare/v0.23.2...v0.23.3) #### What's Changed - 🐛 Ensure DefaulterRemoveUnknownOrOmitableFields is still working even if objects are equal by [@​k8s-infra-cherrypick-robot](https://redirect.github.com/k8s-infra-cherrypick-robot) in [#​3469](https://redirect.github.com/kubernetes-sigs/controller-runtime/pull/3469) **Full Changelog**: ### [`v0.23.2`](https://redirect.github.com/kubernetes-sigs/controller-runtime/releases/tag/v0.23.2) [Compare Source](https://redirect.github.com/kubernetes-sigs/controller-runtime/compare/v0.23.1...v0.23.2) #### What's Changed - 🐛 Fix fake client's SSA status patch resource version check by [@​k8s-infra-cherrypick-robot](https://redirect.github.com/k8s-infra-cherrypick-robot) in [#​3446](https://redirect.github.com/kubernetes-sigs/controller-runtime/pull/3446) - ✨ Reduce memory usage of default webhooks by [@​k8s-infra-cherrypick-robot](https://redirect.github.com/k8s-infra-cherrypick-robot) in [#​3467](https://redirect.github.com/kubernetes-sigs/controller-runtime/pull/3467) **Full Changelog**:
--- ### 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. 👻 **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: 8d1f1654a49db2beee2aa15c7d517b73fd4f6830 --- go.mod | 127 +++++++++++++++--------------- go.sum | 244 ++++++++++++++++++++++++++++----------------------------- 2 files changed, 184 insertions(+), 187 deletions(-) diff --git a/go.mod b/go.mod index 8190712f..1e277e4d 100644 --- a/go.mod +++ b/go.mod @@ -13,12 +13,12 @@ require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 buf.build/go/protovalidate v1.1.3 charm.land/lipgloss/v2 v2.0.0 - cloud.google.com/go/aiplatform v1.118.0 + cloud.google.com/go/aiplatform v1.119.0 cloud.google.com/go/auth v0.18.2 cloud.google.com/go/bigquery v1.74.0 cloud.google.com/go/bigtable v1.42.0 cloud.google.com/go/certificatemanager v1.9.6 - cloud.google.com/go/compute v1.55.0 + cloud.google.com/go/compute v1.56.0 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 @@ -65,39 +65,39 @@ require ( github.com/antihax/optional v1.0.0 github.com/auth0/go-auth0/v2 v2.6.0 github.com/auth0/go-jwt-middleware/v2 v2.3.1 - github.com/aws/aws-sdk-go-v2 v1.41.2 - github.com/aws/aws-sdk-go-v2/config v1.32.10 - github.com/aws/aws-sdk-go-v2/credentials v1.19.10 - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 - github.com/aws/aws-sdk-go-v2/service/apigateway v1.38.5 - github.com/aws/aws-sdk-go-v2/service/autoscaling v1.64.1 - github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.1 - github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.55.0 - github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.12 - github.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.0 - github.com/aws/aws-sdk-go-v2/service/ec2 v1.293.0 - github.com/aws/aws-sdk-go-v2/service/ecs v1.73.0 - github.com/aws/aws-sdk-go-v2/service/efs v1.41.11 - github.com/aws/aws-sdk-go-v2/service/eks v1.80.1 - github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.20 - github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.7 - github.com/aws/aws-sdk-go-v2/service/iam v1.53.3 - github.com/aws/aws-sdk-go-v2/service/kms v1.50.1 - github.com/aws/aws-sdk-go-v2/service/lambda v1.88.1 - github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.4 - github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.5 - github.com/aws/aws-sdk-go-v2/service/rds v1.116.1 - github.com/aws/aws-sdk-go-v2/service/route53 v1.62.2 - github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 - github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.2 - github.com/aws/aws-sdk-go-v2/service/sns v1.39.12 - github.com/aws/aws-sdk-go-v2/service/sqs v1.42.22 - github.com/aws/aws-sdk-go-v2/service/ssm v1.68.1 - github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 + github.com/aws/aws-sdk-go-v2 v1.41.3 + github.com/aws/aws-sdk-go-v2/config v1.32.11 + github.com/aws/aws-sdk-go-v2/credentials v1.19.11 + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 + github.com/aws/aws-sdk-go-v2/service/apigateway v1.38.6 + github.com/aws/aws-sdk-go-v2/service/autoscaling v1.64.2 + github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.2 + github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.55.1 + github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.13 + github.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.1 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.294.0 + github.com/aws/aws-sdk-go-v2/service/ecs v1.73.1 + github.com/aws/aws-sdk-go-v2/service/efs v1.41.12 + github.com/aws/aws-sdk-go-v2/service/eks v1.80.2 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.21 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.8 + github.com/aws/aws-sdk-go-v2/service/iam v1.53.4 + github.com/aws/aws-sdk-go-v2/service/kms v1.50.2 + github.com/aws/aws-sdk-go-v2/service/lambda v1.88.2 + github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.5 + github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.6 + github.com/aws/aws-sdk-go-v2/service/rds v1.116.2 + github.com/aws/aws-sdk-go-v2/service/route53 v1.62.3 + github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 + github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.4 + github.com/aws/aws-sdk-go-v2/service/sns v1.39.13 + github.com/aws/aws-sdk-go-v2/service/sqs v1.42.23 + github.com/aws/aws-sdk-go-v2/service/ssm v1.68.2 + github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 github.com/aws/smithy-go v1.24.2 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/brianvoe/gofakeit/v7 v7.14.1 github.com/cenkalti/backoff/v5 v5.0.3 github.com/charmbracelet/glamour v0.10.0 github.com/coder/websocket v1.8.14 @@ -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.12 + github.com/harness/harness-go-sdk v0.7.13 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-20260224005459-813a97530220 @@ -126,7 +126,7 @@ require ( 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.3 + github.com/micahhausler/aws-iam-policy v0.4.4 github.com/miekg/dns v1.1.72 github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-ps v1.0.0 @@ -138,7 +138,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.24.0 + github.com/openai/openai-go/v3 v3.26.0 github.com/openrdap/rdap v0.9.2-0.20240517203139-eb57b3a8dedd github.com/overmindtech/otelpgx v0.10.1-0.20260303210427-65bf1016045e github.com/overmindtech/pterm v0.0.0-20240919144758-04d94ccb2297 @@ -170,13 +170,13 @@ require ( github.com/zclconf/go-cty v1.18.0 go.etcd.io/bbolt v1.4.3 go.opentelemetry.io/contrib/detectors/aws/ec2/v2 v2.0.0-20250901115419-474a7992e57c - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 - go.opentelemetry.io/otel v1.40.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 - go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.40.0 - go.opentelemetry.io/otel/sdk v1.40.0 - go.opentelemetry.io/otel/trace v1.40.0 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0 + go.opentelemetry.io/otel v1.41.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.41.0 + go.opentelemetry.io/otel/sdk v1.41.0 + go.opentelemetry.io/otel/trace v1.41.0 go.uber.org/automaxprocs v1.6.0 go.uber.org/goleak v1.3.0 go.uber.org/mock v0.6.0 @@ -188,7 +188,7 @@ require ( google.golang.org/api v0.269.0 google.golang.org/genproto v0.0.0-20260128011058-8636f8732409 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 - google.golang.org/grpc v1.79.1 + google.golang.org/grpc v1.79.2 google.golang.org/protobuf v1.36.11 gopkg.in/ini.v1 v1.67.1 gopkg.in/yaml.v3 v3.0.1 @@ -198,7 +198,7 @@ require ( k8s.io/component-base v0.35.2 modernc.org/sqlite v1.46.1 riverqueue.com/riverui v0.15.0 - sigs.k8s.io/controller-runtime v0.23.1 + sigs.k8s.io/controller-runtime v0.23.3 sigs.k8s.io/kind v0.31.0 ) @@ -234,19 +234,19 @@ require ( 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.5 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.18 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // 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 @@ -327,7 +327,7 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.12 // 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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // 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 @@ -481,9 +481,9 @@ require ( 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/metric v1.41.0 // indirect go.opentelemetry.io/otel/schema v0.0.12 // indirect - go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.41.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 @@ -501,7 +501,7 @@ require ( 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 + google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // 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 @@ -521,7 +521,4 @@ require ( sigs.k8s.io/yaml v1.6.0 // indirect ) -require ( - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect - github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect -) +require github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 diff --git a/go.sum b/go.sum index bf6c7871..92e23c81 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,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.118.0 h1:c0HD2VpKurq/H+k5/EuZt9kA5s7mfSce4DM/Mc4hXag= -cloud.google.com/go/aiplatform v1.118.0/go.mod h1:27DcZJbaxFntewF6O0HojDE1B8JQOGKYopNjwoICFdI= +cloud.google.com/go/aiplatform v1.119.0 h1:Fum1ighlxsmwbmaf0nhuMDebcKJkpx2mgmd1YcyXaYY= +cloud.google.com/go/aiplatform v1.119.0/go.mod h1:27DcZJbaxFntewF6O0HojDE1B8JQOGKYopNjwoICFdI= cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= @@ -43,8 +43,8 @@ cloud.google.com/go/bigtable v1.42.0 h1:SREvT4jLhJQZXUjsLmFs/1SMQJ+rKEj1cJuPE9li 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.55.0 h1:1roY8Wqzi8EgDPFJ8SI2v+TI7DodHNn94xQ4fvx10XU= -cloud.google.com/go/compute v1.55.0/go.mod h1:fMFC0mRv+fW2ISg7M3tpDfpZ+kkrHpC/ImNFRCYiNK0= +cloud.google.com/go/compute v1.56.0 h1:e8xch/mR0tJoUBj3nhNb96+MOQ1JGVGB+rBfVzWEU5I= +cloud.google.com/go/compute v1.56.0/go.mod h1:fMFC0mRv+fW2ISg7M3tpDfpZ+kkrHpC/ImNFRCYiNK0= 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= @@ -232,90 +232,90 @@ github.com/auth0/go-auth0/v2 v2.6.0 h1:KCoLxTcH8qXPYbwKZxxFrL/6P+P+Zc58BQPL6w0Kt github.com/auth0/go-auth0/v2 v2.6.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.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= -github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c= -github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= -github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= -github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4= -github.com/aws/aws-sdk-go-v2/service/apigateway v1.38.5 h1:YQq9Nc7b1u4qIwUPQACr59mPCW3Gfb8QwFL7r4PxOP4= -github.com/aws/aws-sdk-go-v2/service/apigateway v1.38.5/go.mod h1:iRxNPQXn19AXRzweQQVRT153qLbmSzW6S6KKQYCYZ5U= -github.com/aws/aws-sdk-go-v2/service/autoscaling v1.64.1 h1:3eD5+Hg+h7XTwmix7vWf5oSIBp/1+KWync+JVsgfWsg= -github.com/aws/aws-sdk-go-v2/service/autoscaling v1.64.1/go.mod h1:c7Rb5WS2TW1nY+Mz60fPTdMAdkpZWCIzHz7HrNdKft8= -github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.1 h1:fwkGr0AyYMq/oxzBrNWVLcmSgSWVyGtFAanNs+ECRes= -github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.1/go.mod h1:PAegJVxp+CkgKZBZVEaTWBN2bHwH24FLl5sIIHYuzOU= -github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.55.0 h1:h3AU/3FXAFLwNFnbQCPSnak46FD69QwiD7OpB+afg3I= -github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.55.0/go.mod h1:SRVEOVD920otumvM08MTqzhQ916eYiDNGpHPB1dqxr8= -github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.12 h1:jTjCUj61HLa3rk/GGnoLjv5iElFDlmdCoXyqAPRxpy4= -github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.12/go.mod h1:TY3yvOssvgQqFskskRdAMQx+waaGKzoFX5HMXtOiRG8= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.0 h1:n5BubZVgbYyweQmdqMT+HMhH07wCxmMyBAQy/VhinoU= -github.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.0/go.mod h1:IFMlDGLL3eM098XqgRk27wateJOnrzp7zz93Wh/F9qk= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.293.0 h1:dgdIaG/GCiXMo16HAdFwpjt9Vn34bD2WVH5SiZdwzUc= -github.com/aws/aws-sdk-go-v2/service/ec2 v1.293.0/go.mod h1:2dMnUs1QzlGzsm46i9oBHAxVHQp7b6qF7PljWcgVEVE= -github.com/aws/aws-sdk-go-v2/service/ecs v1.73.0 h1:bZAxMktXWPmeWhB6I14LsJE2e+t6uLASV80xZdqqXlk= -github.com/aws/aws-sdk-go-v2/service/ecs v1.73.0/go.mod h1:DdtkqcURi9GM8f9HVLzJLTvS0h0k1qYg39vKQFmeR/k= -github.com/aws/aws-sdk-go-v2/service/efs v1.41.11 h1:tCCyWJmkqYJbdfS4Dm3Pyg07b1kp1wCcTgY6Q+FPvU0= -github.com/aws/aws-sdk-go-v2/service/efs v1.41.11/go.mod h1:sVhXa89shXJ36cMmBJPiPi8+s5NCO6gnnlKjjoGrL6s= -github.com/aws/aws-sdk-go-v2/service/eks v1.80.1 h1:Aivj88+23MYkW/B507eqsnLHTMmj4A/Us2AxKz+PDkM= -github.com/aws/aws-sdk-go-v2/service/eks v1.80.1/go.mod h1:p30UgulgoiPvwWGGfVeiaCbOzD1PTObBVYn6MmCPHVg= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.20 h1:kHQywC96ZviLmJJmgWKm6NTGX1BR3hEv52Gl82ik0i0= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.20/go.mod h1:bsLJBZhd8V2OqgNFn61nVh6PTluA4JZh+/DIneIntw4= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.7 h1:txeoy+BxL/Xef6Cl8zAq4ZewY7c+KnQ3gPSMSTTkTt4= -github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.7/go.mod h1:tv2v97S1V5kkp/1vneSYad5Cnrbo+4vfiNNAKCWNKIk= -github.com/aws/aws-sdk-go-v2/service/iam v1.53.3 h1:boKZv8dNdHznhAA68hb/dqFz5pxoWmRAOJr9LtscVCI= -github.com/aws/aws-sdk-go-v2/service/iam v1.53.3/go.mod h1:E0QHh3aEwxYb7xshjvxYDELiOda7KBYJ77e/TvGhpcM= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.18 h1:J8H6iJPIb40gWCjAHfFCCergiy94TuJ5bFxaF+OGRcY= -github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.18/go.mod h1:59002AlnnGT2qznAiC0Hi+WhheaEWTiWyAeA9DQf0/w= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY= -github.com/aws/aws-sdk-go-v2/service/kms v1.50.1 h1:wb/PYYm3wlcqGzw7Ls4GD3X5+seDDoNdVYIB6I/V87E= -github.com/aws/aws-sdk-go-v2/service/kms v1.50.1/go.mod h1:xvHowJ6J9CuaFE04S8fitWQXytf4sHz3DTPGhw9FtmU= -github.com/aws/aws-sdk-go-v2/service/lambda v1.88.1 h1:9WZiZ+1YXpvqvOi2CszopJJlzvv2h8cpxzPBy/rF+NA= -github.com/aws/aws-sdk-go-v2/service/lambda v1.88.1/go.mod h1:NFUHqj4J37VOyZvFHoMn4FjSBaFsPEHeTaBup0isZWM= -github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.4 h1:cWSEfMOtr75M7eecGNqqGS250cbddYSKmS6i35vm+EI= -github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.4/go.mod h1:YdidU5yM1JjqRMfLm8P0wsR3hdGsBUFffpnv66HCtvU= -github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.5 h1:35askM1RKc8/3JaO0Ad/wTzSgmw6/Z9KDX8/HQV/5XM= -github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.5/go.mod h1:cI5BdcbX2OU4pnvhgo/Wx0L2ODiq975nNpu7FkyWqW4= -github.com/aws/aws-sdk-go-v2/service/rds v1.116.1 h1:a5PMhM3lOcu2DKgvYGjhCDToKQnz9VEUo9iSc5+DsyA= -github.com/aws/aws-sdk-go-v2/service/rds v1.116.1/go.mod h1:bMaMwbVQ96bx42kDw/Ko+YiDyT/UCotPO+1RDp6lq7E= -github.com/aws/aws-sdk-go-v2/service/route53 v1.62.2 h1:zoD/SoiVQi8l8tuQn//VexrXS2yorg/+717JNA4Ble8= -github.com/aws/aws-sdk-go-v2/service/route53 v1.62.2/go.mod h1:Ll1DCasPTBFtHK5t/U5WIwGIyRuY3xY+x8/LmqIlqpM= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0= -github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.2 h1:MJ6IIv3VdXESqoORpAgQJYSWLrY7G1AuT8XBQKWCUq8= -github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.2/go.mod h1:Qj7f4iKqd4n/UKcuWwlFhd1irk6S3H27r8QpfVItCZc= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= -github.com/aws/aws-sdk-go-v2/service/sns v1.39.12 h1:yVf0R6Mp8iXmy3/yCY97YyHB1VSkxlxK0ywh14tGuuk= -github.com/aws/aws-sdk-go-v2/service/sns v1.39.12/go.mod h1:9pHipxPwPZJcYm1TEU4gBzwcceAREvks2GDGJewm8Lo= -github.com/aws/aws-sdk-go-v2/service/sqs v1.42.22 h1:CVksqT2e8RFAixRTlDqu1nj174Vjb3VqG7wyZEAlYuA= -github.com/aws/aws-sdk-go-v2/service/sqs v1.42.22/go.mod h1:n3/KSi68g5s54U9J1FV4fRz8oK+7ML2RJK+mDu6gGS0= -github.com/aws/aws-sdk-go-v2/service/ssm v1.68.1 h1:kDgdZuYBWSsh3U/jZOXwcqfX6UsSzFcmtgKx7C0c5/E= -github.com/aws/aws-sdk-go-v2/service/ssm v1.68.1/go.mod h1:xyao5chroDlX/9q/rKBxRKZPv9NdG5Pm9W5zS+wQJ84= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= +github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA= +github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs= +github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo= +github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc= +github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c= +github.com/aws/aws-sdk-go-v2/service/apigateway v1.38.6 h1:dzd86UudvxJ1c6z/o+hHh7ZhkoBrh81XYz/M11zwQYI= +github.com/aws/aws-sdk-go-v2/service/apigateway v1.38.6/go.mod h1:jWmyEnBPJdt+RaHSRzZDKp3HyyzjOofGp4+xXY503Do= +github.com/aws/aws-sdk-go-v2/service/autoscaling v1.64.2 h1:pzFtdV2DArJul6aM3+WiWjUQ63IzrSnSbvBr8FAokt4= +github.com/aws/aws-sdk-go-v2/service/autoscaling v1.64.2/go.mod h1:8xQlcle6cf4R66HrXbiahORXakWpLlvJXoiGae5BlIc= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.2 h1:+5lijyTp+IoU5oh6rL3374yEkaPeFnaes+b4WWUQC2I= +github.com/aws/aws-sdk-go-v2/service/cloudfront v1.60.2/go.mod h1:Ndq7ECdcXc8jmE4WPhl409BdAAWW6jrirMFgliMxMtU= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.55.1 h1:s+ZS2lmYFeCISy20RkSerTmfMIzxlevj4LyWNuE3cfY= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.55.1/go.mod h1:xXUsqpyas4oCIPxrKoCeqvyvFBLEYSohybRVV0bHq9A= +github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.13 h1:nUrVaHNZ82u7H3012w+gqscrbFjVLfSFWvbgeP7+J90= +github.com/aws/aws-sdk-go-v2/service/directconnect v1.38.13/go.mod h1:DTuhYIuDsUBwdSHh6Dg8NNRq7CCeVPI8w0D/ZXQiE40= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.1 h1:EkW4NqA2mwCkL7YCDYh6OpA/bCMhKYbZgpRHt2FD2Ow= +github.com/aws/aws-sdk-go-v2/service/dynamodb v1.56.1/go.mod h1:OQp5333OH1IjmJmJpTU4IwoaOoCMnDrThg0zIx169rE= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.294.0 h1:776KnBqePBBR6zEDi0bUIHXzUBOISa2WgAKEgckUF8M= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.294.0/go.mod h1:rB577GvkmJADVOFGY8/j9sPv/ewcsEtQNsd9Lrn7Zx0= +github.com/aws/aws-sdk-go-v2/service/ecs v1.73.1 h1:TSmcWx+RzhGJrPNoFkuqANafJQ7xY3W2UBg6ShN3ae8= +github.com/aws/aws-sdk-go-v2/service/ecs v1.73.1/go.mod h1:KWILGx+bRowcGyJU/va2Ift48c658blP5e1qvldnIRE= +github.com/aws/aws-sdk-go-v2/service/efs v1.41.12 h1:YZXW11dESIf6CNhMG2ICZonCkzKBaGLuFamSJTYV5g0= +github.com/aws/aws-sdk-go-v2/service/efs v1.41.12/go.mod h1:+rjniKD0YQAmjiDNJvLodKXn1vXWwMpctrr/M4zm1V4= +github.com/aws/aws-sdk-go-v2/service/eks v1.80.2 h1:+FLU7+D9AW9ZMQIg4YjIN/nTJV0A2TIB2f+ovZXqAdU= +github.com/aws/aws-sdk-go-v2/service/eks v1.80.2/go.mod h1:nx52u/3RVDWkOcrAchYgt7CXkrd03A6Gvzi0trtMFjQ= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.21 h1:VriOdPKF8YrkMpnT76ZwA2LXk5aBInOfuzN14QGTOJc= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.33.21/go.mod h1:sp4Mz5YUnYCvIkGNEcdEPp+DuHqquEZYXyIuKXuHzig= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.8 h1:xUwbqWhKASQsigeQfeBjhbm6dAP1EeTulHnNSYv5Xfc= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.54.8/go.mod h1:sQoz/dTooY3kCkNNGxVLTS7EacLA0qXUaK4BkpMjGOc= +github.com/aws/aws-sdk-go-v2/service/iam v1.53.4 h1:FUWGS7m97SYL0bk9Kb+Q4bVpcSrKOHNiIbEXIRFTRW4= +github.com/aws/aws-sdk-go-v2/service/iam v1.53.4/go.mod h1:seDE466zJ4haVuAVcRk+yIH4DWb3s6cqt3Od8GxnGAA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.19 h1:jdCj9vbCXwzTcIJX+MVd2UdssFhRJFTrWlPZwZB8Hpk= +github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.11.19/go.mod h1:Dgg2d5WGRr7YB8JJsELskBxLUhgwWppXPwlvmuQKhbc= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.2 h1:UOHOXigIzDRaEU03CBQcZ5uW7FNC7E+vwfhsQWXl5RQ= +github.com/aws/aws-sdk-go-v2/service/kms v1.50.2/go.mod h1:nAa5gmcmAmjXN3tGuhPSHLXFeWv+7nzKhjZzh8F7MH0= +github.com/aws/aws-sdk-go-v2/service/lambda v1.88.2 h1:j+IFEtr7aykD6jJRE86kv/+TgN1UK90LudBuz2bjjYw= +github.com/aws/aws-sdk-go-v2/service/lambda v1.88.2/go.mod h1:IDvS3hFp41ZJTByY7BO8PNgQkPNeQDjJfU/0cHJ2V4o= +github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.5 h1:atVRUNiG3hrpntduj0OExYB31F59zr+eavoAecVNMhQ= +github.com/aws/aws-sdk-go-v2/service/networkfirewall v1.59.5/go.mod h1:Lr/sslNngRPyPo2FeWkEo02t9f/CjkzSIeR0MqRh8ao= +github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.6 h1:G6+LJP+mxaLuM65jEwpgOcef2fmGSyr92U0zjE988A0= +github.com/aws/aws-sdk-go-v2/service/networkmanager v1.41.6/go.mod h1:GIy6ofSymO4ZAPKlYhb6Na4sXqsJrmPZPP/NyheE0rk= +github.com/aws/aws-sdk-go-v2/service/rds v1.116.2 h1:KQLPCn9BWXW0Y8DyzEokbTF9HOiOQoR77Eu9GKcjBWU= +github.com/aws/aws-sdk-go-v2/service/rds v1.116.2/go.mod h1:aPw0arz1e+cZUbF4LU7ZMYB1ZSYsJKi/tsAq9wADfeE= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.3 h1:JRPXnIr0WwFsSHBmuCvT/uh0Vgys+crvwkOghbJEqi8= +github.com/aws/aws-sdk-go-v2/service/route53 v1.62.3/go.mod h1:DHddp7OO4bY467WVCqWBzk5+aEWn7vqYkap7UigJzGk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk= +github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA= +github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.4 h1:PEz6RPI6hG3GHiaMPmrh4iM684GOdIrEc9L30FaSC2k= +github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.4/go.mod h1:4dOflh7HfqHcjF1OIlt9Tr1T0rDsh906Yc75lAa2CJI= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU= +github.com/aws/aws-sdk-go-v2/service/sns v1.39.13 h1:8xP94tDzFpgwIOsusGiEFHPaqrpckDojoErk/ZFZTio= +github.com/aws/aws-sdk-go-v2/service/sns v1.39.13/go.mod h1:RwF6Xnba8PlINxJUQq1IAWeon6IglvqsnhNqV8QsQjk= +github.com/aws/aws-sdk-go-v2/service/sqs v1.42.23 h1:Rw3+8VaLH0jozccNR52bSvCPYtkiQeNn576l7HCHvL0= +github.com/aws/aws-sdk-go-v2/service/sqs v1.42.23/go.mod h1:MdjRkQEd2EUOiifYnkg/6f1NGtZSN3dFOLNByzufXok= +github.com/aws/aws-sdk-go-v2/service/ssm v1.68.2 h1:idKv7B7NjmTDd05YHQYMMEFNeD0rWxs/kVX4lsjEiDo= +github.com/aws/aws-sdk-go-v2/service/ssm v1.68.2/go.mod h1:1NiL45h4A60CO/hu/UdNyG5AD3VEsdpaQx1l5KtpurA= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aybabtme/iocontrol v0.0.0-20150809002002-ad15bcfc95a0 h1:0NmehRCgyk5rljDQLKUO+cRJCnduDyn11+zGZIc9Z48= @@ -350,8 +350,8 @@ github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+Ar 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/brianvoe/gofakeit/v7 v7.14.1 h1:a7fe3fonbj0cW3wgl5VwIKfZtiH9C3cLnwcIXWT7sow= +github.com/brianvoe/gofakeit/v7 v7.14.1/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= @@ -629,12 +629,12 @@ 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/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= 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.12 h1:CR/0NJljatoeDOnhrZ1otYj8cOB0ge+OD3hSU0Xat5Q= -github.com/harness/harness-go-sdk v0.7.12/go.mod h1:iEAGFfIm0MOFJxN6tqMQSPZiEO/Dz1joLDHrkEU3lps= +github.com/harness/harness-go-sdk v0.7.13 h1:lLiliIivyXW/8L7n244q45hdVKNCYprnfztyes4ew7k= +github.com/harness/harness-go-sdk v0.7.13/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= @@ -812,8 +812,8 @@ github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3v 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.3 h1:TdJamVZxIyycdBR84JfI8+TVdPJ/3PEprWFbj6UdxaI= -github.com/micahhausler/aws-iam-policy v0.4.3/go.mod h1:H+yWljTu4XWJjNJJYgrPUai0AUTGNHc8pumkN57/foI= +github.com/micahhausler/aws-iam-policy v0.4.4 h1:1aMhJ+0CkvUJ8HGN1chX+noXHs8uvGLkD7xIBeYd31c= +github.com/micahhausler/aws-iam-policy v0.4.4/go.mod h1:H+yWljTu4XWJjNJJYgrPUai0AUTGNHc8pumkN57/foI= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= @@ -882,8 +882,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.24.0 h1:08x6GnYiB+AAejTo6yzPY8RkZMJQ8NpreiOyM5QfyYU= -github.com/openai/openai-go/v3 v3.24.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE= +github.com/openai/openai-go/v3 v3.26.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/otelpgx v0.10.1-0.20260303210427-65bf1016045e h1:vP/Zs8Nbd902stVf7hBOd3VP/lIECgAjWR8pNBwcOu4= @@ -1209,32 +1209,32 @@ go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK2 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= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -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/contrib/instrumentation/net/http/otelhttp v0.66.0 h1:PnV4kVnw0zOmwwFkAzCN5O07fw1YOIQor120zrh0AVo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.66.0/go.mod h1:ofAwF4uinaf8SXdVzzbL4OsxJ3VfeEg3f/F6CeF49/Y= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls= 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/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= 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/exporters/stdout/stdouttrace v1.41.0 h1:61oRQmYGMW7pXmFjPg1Muy84ndqMxQ6SH2L8fBG8fSY= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.41.0/go.mod h1:c0z2ubK4RQL+kSDuuFu9WnuXimObon3IiKjJf4NACvU= go.opentelemetry.io/otel/log v0.11.0 h1:c24Hrlk5WJ8JWcwbQxdBqxZdOK7PcP/LFtOtwpDTe3Y= go.opentelemetry.io/otel/log v0.11.0/go.mod h1:U/sxQ83FPmT29trrifhQg+Zj2lo1/IPN1PF6RTFqdwc= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/schema v0.0.12 h1:X8NKrwH07Oe9SJruY/D1XmwHrb6D2+qrLs2POlZX7F4= go.opentelemetry.io/otel/schema v0.0.12/go.mod h1:+w+Q7DdGfykSNi+UU9GAQz5/rtYND6FkBJUWUXzZb0M= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= +go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= +go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= +go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= @@ -1498,8 +1498,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-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/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= +google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -1509,8 +1509,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.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/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= @@ -1605,8 +1605,8 @@ 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/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= +sigs.k8s.io/controller-runtime v0.23.3/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= From 35863ff632d804700df0a975de5b4839a45ded9d Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Fri, 6 Mar 2026 11:13:55 +0100 Subject: [PATCH 67/74] [ENG-2977] Implement BoltDB hash-based sharding for sdpcache (#4157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Introduce `ShardedCache` wrapping 17 independent BoltDB files to eliminate the single-writer bottleneck that caused 345+ goroutines to serialize on one BoltDB write lock in production - GET queries route to exactly one shard via FNV-32a hashing; LIST/SEARCH fan out to all shards in parallel and merge results - `NewCache()` now returns a `ShardedCache` by default — the `Cache` interface is unchanged ## Linear Ticket - **Ticket**: [ENG-2977](https://linear.app/overmind/issue/ENG-2977) — BoltDB Hash-Based Sharding Implementation Plan - **Purpose**: Eliminate BoltDB write contention as a pool-saturation amplifier in source pods (stdlib: 345 blocked goroutines, aws: ~30) - **Priority**: Urgent - **Related**: ENG-2927 (Change analysis graceful timeouts) ## Changes ### New files - **`go/sdpcache/sharded_cache.go`** — `ShardedCache` struct, `NewShardedCache()`, FNV-32a shard routing, fan-out search, OTel attributes (`ovm.cache.shardIndex`, `ovm.cache.shardCount`, `ovm.cache.fanOut`, `ovm.cache.fanOutMaxMs`, `ovm.cache.shardsWithResults`) - **`go/sdpcache/sharded_cache_test.go`** — Shard distribution uniformity (chi-squared), GET routing, LIST fan-out, cross-shard LIST, pendingWork dedup, concurrent write throughput, error routing, benchmark vs single BoltCache ### Modified files - **`go/sdpcache/bolt_cache.go`** — Exported `Search()` method (thin wrapper around internal `search()`) - **`go/sdpcache/cache.go`** — `NewCache()` now calls `newShardedCacheForProduction()` instead of creating a single BoltCache - **`go/sdpcache/cache_test.go`** — Added `ShardedCache` to `cacheImplementations()` and `testSearch()` type switch - **`go/sdpcache/README.md`** — Updated to reflect ShardedCache as the default implementation ## Deviations from Approved Plan Implementation matches the approved plan — no material deviations. Specifically: - All files listed under "Files to Create" and "Files to Modify" were implemented as specified - `DefaultShardCount = 17`, FNV-32a shard routing, parallel fan-out for LIST/SEARCH, shard-0 default for LIST/SEARCH errors — all match the plan - `pendingWork` ownership at ShardedCache level, per-shard `CompactThreshold` scaling (`1GB / 17`), parallel open/close — all match - All OTel attributes from the plan are emitted on the correct spans - All specified tests (distribution uniformity, GET routing, LIST fan-out, cross-shard LIST, pendingWork dedup, concurrent throughput benchmark, CloseAndDestroy cleanup) are present - `go fix` was applied post-implementation, which simplified the `perShardThreshold` clamping to use the built-in `max()` function — a trivial style improvement, not a deviation Made with [Cursor](https://cursor.com) --- > [!NOTE] > **High Risk** > Changes the default production cache backend from a single BoltDB file to a sharded fan-out implementation and refactors shared lookup/dedup logic, which can affect cache hit/miss behavior, result ordering for LIST/SEARCH, and performance under concurrency. > > **Overview** > **`sdpcache.NewCache()` now returns a BoltDB-backed `ShardedCache` by default**, creating multiple BoltDB shard files for improved write concurrency and falling back to `MemoryCache` on initialization failure. > > To support this, BoltDB storage is split into a reusable `boltCacheStore`, and a new `lookupCoordinator` centralizes `Lookup()` behavior (pending-work dedup, re-check logic, GET cardinality enforcement) so shards use raw `Search()` reads and dedup remains top-level. Tests and docs are updated to cover `ShardedCache` (routing, fan-out merge, dedup, purge aggregation, cleanup), and one GCP manual test is relaxed to not assume deterministic LIST ordering. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 952572b1a95a294abb5716323bb01d18c95f0009. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 749c70af4a49b0e842ff46e43cffe2bbef6d0dac --- go/sdpcache/bolt_cache.go | 10 +- go/sdpcache/cache.go | 37 +- go/sdpcache/cache_test.go | 17 +- go/sdpcache/sharded_cache.go | 511 ++++++++++++++ go/sdpcache/sharded_cache_test.go | 659 ++++++++++++++++++ .../gcp/manual/cloud-kms-crypto-key_test.go | 20 +- 6 files changed, 1211 insertions(+), 43 deletions(-) create mode 100644 go/sdpcache/sharded_cache.go create mode 100644 go/sdpcache/sharded_cache_test.go diff --git a/go/sdpcache/bolt_cache.go b/go/sdpcache/bolt_cache.go index 5034ed06..de81b8d6 100644 --- a/go/sdpcache/bolt_cache.go +++ b/go/sdpcache/bolt_cache.go @@ -229,7 +229,7 @@ func WithCompactThreshold(bytes int64) BoltCacheOption { func NewBoltCache(path string, opts ...BoltCacheOption) (*BoltCache, error) { // Ensure the directory exists dir := filepath.Dir(path) - if err := os.MkdirAll(dir, 0o755); err != nil { //nolint:gosec // G301 (path traversal): path comes from application config (NewBoltCache callers), not user HTTP input + if err := os.MkdirAll(dir, 0o755); err != nil { return nil, fmt.Errorf("failed to create directory: %w", err) } @@ -674,7 +674,13 @@ func (c *BoltCache) Lookup(ctx context.Context, srcName string, method sdp.Query return true, ck, items, nil, noopDone } -// Search performs a lower-level search using a CacheKey. +// Search performs a lower-level search using a CacheKey, bypassing pendingWork +// deduplication. This is used by ShardedCache to do raw reads on individual shards. +func (c *BoltCache) Search(ctx context.Context, ck CacheKey) ([]*sdp.Item, error) { + return c.search(ctx, ck) +} + +// 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 { diff --git a/go/sdpcache/cache.go b/go/sdpcache/cache.go index 9655c38f..ceea187b 100644 --- a/go/sdpcache/cache.go +++ b/go/sdpcache/cache.go @@ -5,12 +5,10 @@ import ( "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" @@ -316,38 +314,11 @@ func NewMemoryCache() *MemoryCache { } } -// 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. +// NewCache creates a new cache. This function returns a Cache interface backed +// by a ShardedCache (N independent BoltDB files) for write concurrency. +// 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()) //nolint:gosec // G304 (path traversal): path generated by os.CreateTemp, not from user input - cache := NewMemoryCache() - cache.StartPurger(ctx) - return cache - } - cache.StartPurger(ctx) - return cache + return newShardedCacheForProduction(ctx) } func newExpiryIndex() *btree.BTreeG[*CachedResult] { diff --git a/go/sdpcache/cache_test.go b/go/sdpcache/cache_test.go index c6c2413b..b8bf5326 100644 --- a/go/sdpcache/cache_test.go +++ b/go/sdpcache/cache_test.go @@ -14,13 +14,15 @@ import ( ) // testSearch is a helper function that calls the internal search method -// on either MemoryCache or BoltCache implementations for testing purposes +// on either MemoryCache, BoltCache, or ShardedCache 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) + case *ShardedCache: + return c.searchByKey(ctx, ck) default: return nil, fmt.Errorf("unsupported cache type for search: %T", cache) } @@ -47,6 +49,19 @@ func cacheImplementations(tb testing.TB) []struct { }) return c }}, + {"ShardedCache", func() Cache { + c, err := NewShardedCache( + filepath.Join(tb.TempDir(), "shards"), + DefaultShardCount, + ) + if err != nil { + tb.Fatalf("failed to create ShardedCache: %v", err) + } + tb.Cleanup(func() { + _ = c.CloseAndDestroy() + }) + return c + }}, } } diff --git a/go/sdpcache/sharded_cache.go b/go/sdpcache/sharded_cache.go new file mode 100644 index 00000000..6d9b30c3 --- /dev/null +++ b/go/sdpcache/sharded_cache.go @@ -0,0 +1,511 @@ +package sdpcache + +import ( + "context" + "errors" + "fmt" + "hash/fnv" + "os" + "path/filepath" + "sync" + "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.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +// DefaultShardCount is the number of independent BoltDB shards. 17 is prime +// (avoids hash collision clustering) and distributes ~345 stdlib goroutines to +// ~20 per shard, making BoltDB's single-writer lock no longer a bottleneck. +const DefaultShardCount = 17 + +// ShardedCache implements the Cache interface by distributing entries across N +// independent BoltCache instances. Shard selection uses FNV-32a of the item +// identity (SSTHash + UniqueAttributeValue), so writes within a single adapter +// type (e.g. DNS in stdlib) spread evenly across all shards. +// +// GET queries route to exactly one shard. LIST/SEARCH queries fan out to all +// shards in parallel and merge results. pendingWork deduplication lives at the +// ShardedCache level to prevent duplicate API calls across the fan-out. +type ShardedCache struct { + shards []*BoltCache + dir string + + // pendingWork lives at the ShardedCache level so that deduplication spans + // the entire cache, not individual shards. + pending *pendingWork +} + +var _ Cache = (*ShardedCache)(nil) + +// NewShardedCache creates N BoltCache instances in dir (shard-00.db through +// shard-{N-1}.db) using goroutine fan-out to avoid N× startup latency. +func NewShardedCache(dir string, shardCount int, opts ...BoltCacheOption) (*ShardedCache, error) { + if shardCount <= 0 { + return nil, fmt.Errorf("shard count must be positive, got %d", shardCount) + } + + if err := os.MkdirAll(dir, 0o755); err != nil { + return nil, fmt.Errorf("failed to create shard directory: %w", err) + } + + shards := make([]*BoltCache, shardCount) + errs := make([]error, shardCount) + + var wg sync.WaitGroup + for i := range shardCount { + wg.Add(1) + go func(idx int) { + defer wg.Done() + path := filepath.Join(dir, fmt.Sprintf("shard-%02d.db", idx)) + c, err := NewBoltCache(path, opts...) + if err != nil { + errs[idx] = fmt.Errorf("shard %d: %w", idx, err) + return + } + shards[idx] = c + }(i) + } + wg.Wait() + + // If any shard failed, close the ones that succeeded and return the error. + for _, err := range errs { + if err != nil { + for _, s := range shards { + if s != nil { + _ = s.CloseAndDestroy() + } + } + return nil, err + } + } + + return &ShardedCache{ + shards: shards, + dir: dir, + pending: newPendingWork(), + }, nil +} + +// shardFor returns the shard index for a given item identity. +func (sc *ShardedCache) shardFor(sstHash SSTHash, uav string) int { + h := fnv.New32a() + _, _ = h.Write([]byte(sstHash)) + _, _ = h.Write([]byte(uav)) + return int(h.Sum32()) % len(sc.shards) +} + +// Lookup performs a cache lookup, routing GET queries to a single shard and +// LIST/SEARCH queries to all shards via parallel fan-out. +func (sc *ShardedCache) 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, "ShardedCache.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), + attribute.Int("ovm.cache.shardCount", len(sc.shards)), + ), + ) + defer span.End() + + ck := CacheKeyFromParts(srcName, method, scope, typ, query) + + 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 := sc.searchByKey(ctx, ck) + + if err != nil { + var qErr *sdp.QueryError + if errors.Is(err, ErrCacheNotFound) { + shouldWork, entry := sc.pending.StartWork(ck.String()) + if shouldWork { + 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, sc.createDoneFunc(ck) + } + + pendingWaitStart := time.Now() + ok := sc.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 { + span.SetAttributes( + attribute.String("ovm.cache.result", "pending work cancelled or timeout"), + attribute.Bool("ovm.cache.hit", false), + ) + return false, ck, nil, nil, noopDone + } + + items, recheckErr := sc.searchByKey(ctx, ck) + if recheckErr != nil { + if errors.Is(recheckErr, ErrCacheNotFound) { + 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 + } + 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 + } + 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), + ) + sc.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 +} + +// searchByKey routes GET queries to a single shard and LIST/SEARCH/unspecified +// queries to all shards via fan-out. +func (sc *ShardedCache) searchByKey(ctx context.Context, ck CacheKey) ([]*sdp.Item, error) { + span := trace.SpanFromContext(ctx) + + if ck.UniqueAttributeValue != nil { + idx := sc.shardFor(ck.SST.Hash(), *ck.UniqueAttributeValue) + span.SetAttributes( + attribute.Int("ovm.cache.shardIndex", idx), + attribute.Bool("ovm.cache.fanOut", false), + ) + return sc.shards[idx].Search(ctx, ck) + } + + return sc.searchAll(ctx, ck) +} + +// searchAll fans out a search to all shards in parallel and merges results. +func (sc *ShardedCache) searchAll(ctx context.Context, ck CacheKey) ([]*sdp.Item, error) { + span := trace.SpanFromContext(ctx) + span.SetAttributes(attribute.Bool("ovm.cache.fanOut", true)) + + type result struct { + items []*sdp.Item + err error + dur time.Duration + } + results := make([]result, len(sc.shards)) + + var wg sync.WaitGroup + for i, shard := range sc.shards { + wg.Add(1) + go func(i int, shard *BoltCache) { + defer wg.Done() + start := time.Now() + items, err := shard.Search(ctx, ck) + results[i] = result{items: items, err: err, dur: time.Since(start)} + }(i, shard) + } + wg.Wait() + + var ( + allItems []*sdp.Item + maxDur time.Duration + shardsWithResult int + firstErr error + allNotFound = true + ) + + for _, r := range results { + if r.dur > maxDur { + maxDur = r.dur + } + if r.err != nil { + if errors.Is(r.err, ErrCacheNotFound) { + continue + } + allNotFound = false + if firstErr == nil { + firstErr = r.err + } + continue + } + allNotFound = false + if len(r.items) > 0 { + shardsWithResult++ + allItems = append(allItems, r.items...) + } + } + + span.SetAttributes( + attribute.Float64("ovm.cache.fanOutMaxMs", float64(maxDur.Milliseconds())), + attribute.Int("ovm.cache.shardsWithResults", shardsWithResult), + ) + + if firstErr != nil { + return nil, firstErr + } + + if allNotFound { + return nil, ErrCacheNotFound + } + + return allItems, nil +} + +// StoreItem routes the item to one shard based on its UniqueAttributeValue. +func (sc *ShardedCache) StoreItem(ctx context.Context, item *sdp.Item, duration time.Duration, ck CacheKey) { + if item == nil { + return + } + + sstHash := ck.SST.Hash() + uav := item.UniqueAttributeValue() + idx := sc.shardFor(sstHash, uav) + + span := trace.SpanFromContext(ctx) + span.SetAttributes(attribute.Int("ovm.cache.shardIndex", idx)) + + sc.shards[idx].StoreItem(ctx, item, duration, ck) +} + +// StoreError routes the error based on the CacheKey: +// - GET errors (UniqueAttributeValue set) go to the same shard a GET Lookup would query. +// - LIST/SEARCH errors go to shard 0 as a deterministic default; fan-out reads will find them. +func (sc *ShardedCache) StoreError(ctx context.Context, err error, duration time.Duration, ck CacheKey) { + if err == nil { + return + } + + var idx int + if ck.UniqueAttributeValue != nil { + idx = sc.shardFor(ck.SST.Hash(), *ck.UniqueAttributeValue) + } + + span := trace.SpanFromContext(ctx) + span.SetAttributes(attribute.Int("ovm.cache.shardIndex", idx)) + + sc.shards[idx].StoreError(ctx, err, duration, ck) +} + +// Delete fans out to all shards. +func (sc *ShardedCache) Delete(ck CacheKey) { + var wg sync.WaitGroup + for _, shard := range sc.shards { + wg.Add(1) + go func(s *BoltCache) { + defer wg.Done() + s.Delete(ck) + }(shard) + } + wg.Wait() +} + +// Clear fans out to all shards. +func (sc *ShardedCache) Clear() { + var wg sync.WaitGroup + for _, shard := range sc.shards { + wg.Add(1) + go func(s *BoltCache) { + defer wg.Done() + s.Clear() + }(shard) + } + wg.Wait() +} + +// Purge fans out to all shards in parallel and aggregates PurgeStats. +// TimeTaken reflects wall-clock time of the parallel fan-out, not the sum of +// per-shard durations. +func (sc *ShardedCache) Purge(ctx context.Context, before time.Time) PurgeStats { + type result struct { + stats PurgeStats + } + results := make([]result, len(sc.shards)) + + start := time.Now() + + var wg sync.WaitGroup + for i, shard := range sc.shards { + wg.Add(1) + go func(i int, s *BoltCache) { + defer wg.Done() + results[i] = result{stats: s.Purge(ctx, before)} + }(i, shard) + } + wg.Wait() + + combined := PurgeStats{ + TimeTaken: time.Since(start), + } + for _, r := range results { + combined.NumPurged += r.stats.NumPurged + if r.stats.NextExpiry != nil { + if combined.NextExpiry == nil || r.stats.NextExpiry.Before(*combined.NextExpiry) { + combined.NextExpiry = r.stats.NextExpiry + } + } + } + return combined +} + +// GetMinWaitTime returns the minimum wait time from the first shard. +func (sc *ShardedCache) GetMinWaitTime() time.Duration { + if len(sc.shards) == 0 { + return 0 + } + return sc.shards[0].GetMinWaitTime() +} + +// StartPurger starts a purger on each shard independently. +func (sc *ShardedCache) StartPurger(ctx context.Context) { + for _, shard := range sc.shards { + shard.StartPurger(ctx) + } +} + +// CloseAndDestroy closes and destroys all shard files in parallel, then removes +// the shard directory. +func (sc *ShardedCache) CloseAndDestroy() error { + errs := make([]error, len(sc.shards)) + + var wg sync.WaitGroup + for i, shard := range sc.shards { + wg.Add(1) + go func(i int, s *BoltCache) { + defer wg.Done() + errs[i] = s.CloseAndDestroy() + }(i, shard) + } + wg.Wait() + + for _, err := range errs { + if err != nil { + return err + } + } + + return os.RemoveAll(sc.dir) +} + +// createDoneFunc returns a done function that calls pending.Complete for the +// given cache key. Safe to call multiple times (idempotent via sync.Once). +func (sc *ShardedCache) createDoneFunc(ck CacheKey) func() { + if sc == nil || sc.pending == nil { + return noopDone + } + key := ck.String() + var once sync.Once + return func() { + once.Do(func() { + sc.pending.Complete(key) + }) + } +} + +// newShardedCacheForProduction is used by NewCache to create a production +// ShardedCache with appropriate defaults. It logs and falls back to MemoryCache +// on failure. +func newShardedCacheForProduction(ctx context.Context) Cache { + dir, err := os.MkdirTemp("", "sdpcache-shards-*") + if err != nil { + sentry.CaptureException(err) + log.WithError(err).Error("Failed to create temp dir for ShardedCache, using memory cache instead") + cache := NewMemoryCache() + cache.StartPurger(ctx) + return cache + } + + perShardThreshold := int64(1*1024*1024*1024) / int64(DefaultShardCount) + + cache, err := NewShardedCache( + dir, + DefaultShardCount, + WithMinWaitTime(30*time.Second), + WithCompactThreshold(perShardThreshold), + ) + if err != nil { + sentry.CaptureException(err) + log.WithError(err).Error("Failed to create ShardedCache, using memory cache instead") + _ = os.RemoveAll(dir) + memCache := NewMemoryCache() + memCache.StartPurger(ctx) + return memCache + } + + cache.StartPurger(ctx) + return cache +} diff --git a/go/sdpcache/sharded_cache_test.go b/go/sdpcache/sharded_cache_test.go new file mode 100644 index 00000000..fd656161 --- /dev/null +++ b/go/sdpcache/sharded_cache_test.go @@ -0,0 +1,659 @@ +package sdpcache + +import ( + "context" + "fmt" + "math" + "os" + "path/filepath" + "sync" + "testing" + "time" + + "github.com/overmindtech/cli/go/sdp-go" +) + +func TestShardDistributionUniformity(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + cache, err := NewShardedCache(dir, DefaultShardCount) + if err != nil { + t.Fatalf("failed to create ShardedCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + ctx := t.Context() + numItems := 1000 + + // Use the same SST for all items so they share the same BoltDB SST bucket. + // Different UAVs cause items to distribute across shards via shardFor(). + sst := SST{SourceName: "test-source", Scope: "scope", Type: "type"} + method := sdp.QueryMethod_LIST + ck := CacheKey{SST: sst, Method: &method} + + for i := range numItems { + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + item.Metadata.SourceName = sst.SourceName + + attrs := make(map[string]any) + attrs["name"] = fmt.Sprintf("item-%d", i) + attributes, _ := sdp.ToAttributes(attrs) + item.Attributes = attributes + + cache.StoreItem(ctx, item, 10*time.Second, ck) + } + + // Count items per shard by searching each shard with the common SST + counts := make([]int, DefaultShardCount) + for i, shard := range cache.shards { + items, searchErr := shard.Search(ctx, ck) + if searchErr == nil { + counts[i] = len(items) + } + } + + totalFound := 0 + for _, c := range counts { + totalFound += c + } + + if totalFound != numItems { + t.Errorf("expected %d total items across shards, got %d", numItems, totalFound) + } + + // Verify distribution is reasonably uniform: no shard should have more than + // 3× the expected average (very loose bound to avoid flaky tests). + expected := float64(numItems) / float64(DefaultShardCount) + for i, c := range counts { + if float64(c) > expected*3 { + t.Errorf("shard %d has %d items, expected roughly %.0f (3× threshold: %.0f)", i, c, expected, expected*3) + } + } + + // Chi-squared test for uniformity (p < 0.001 threshold) + var chiSq float64 + for _, c := range counts { + diff := float64(c) - expected + chiSq += (diff * diff) / expected + } + // Critical value for df=16, p=0.001 is ~39.25 + if chiSq > 39.25 { + t.Errorf("chi-squared %.2f exceeds critical value 39.25 (df=16, p=0.001), distribution may be non-uniform: %v", chiSq, counts) + } +} + +func TestShardedCacheGETRoutesToCorrectShard(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + cache, err := NewShardedCache(dir, DefaultShardCount) + if err != nil { + t.Fatalf("failed to create ShardedCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + ctx := t.Context() + sst := SST{SourceName: "test", Scope: "scope", Type: "type"} + method := sdp.QueryMethod_GET + + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + item.Metadata.SourceName = sst.SourceName + + uav := item.UniqueAttributeValue() + ck := CacheKey{SST: sst, Method: &method, UniqueAttributeValue: &uav} + cache.StoreItem(ctx, item, 10*time.Second, ck) + + // Verify the item lands on the expected shard + expectedShard := cache.shardFor(sst.Hash(), uav) + items, err := cache.shards[expectedShard].Search(ctx, ck) + if err != nil { + t.Fatalf("expected item on shard %d, got error: %v", expectedShard, err) + } + if len(items) != 1 { + t.Fatalf("expected 1 item on shard %d, got %d", expectedShard, len(items)) + } + + // Verify Lookup returns the item + hit, _, cachedItems, qErr, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, uav, false) + defer done() + if qErr != nil { + t.Fatalf("unexpected error: %v", qErr) + } + if !hit { + t.Fatal("expected cache hit") + } + if len(cachedItems) != 1 { + t.Fatalf("expected 1 item, got %d", len(cachedItems)) + } +} + +func TestShardedCacheLISTFanOutMerge(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + cache, err := NewShardedCache(dir, DefaultShardCount) + if err != nil { + t.Fatalf("failed to create ShardedCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + ctx := t.Context() + sst := SST{SourceName: "test", Scope: "scope", Type: "type"} + method := sdp.QueryMethod_LIST + ck := CacheKey{SST: sst, Method: &method} + + // Store items that should land on different shards + numItems := 50 + for i := range numItems { + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + item.Metadata.SourceName = sst.SourceName + + attrs := make(map[string]any) + attrs["name"] = fmt.Sprintf("item-%d", i) + attributes, _ := sdp.ToAttributes(attrs) + item.Attributes = attributes + + cache.StoreItem(ctx, item, 10*time.Second, ck) + } + + // LIST should fan out and return all items + hit, _, items, qErr, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, "", false) + defer done() + if qErr != nil { + t.Fatalf("unexpected error: %v", qErr) + } + if !hit { + t.Fatal("expected cache hit") + } + if len(items) != numItems { + t.Errorf("expected %d items from LIST fan-out, got %d", numItems, len(items)) + } +} + +func TestShardedCacheCrossShardLIST(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + // Use a small shard count for easier verification + cache, err := NewShardedCache(dir, 3) + if err != nil { + t.Fatalf("failed to create ShardedCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + ctx := t.Context() + sst := SST{SourceName: "test", Scope: "scope", Type: "type"} + method := sdp.QueryMethod_LIST + ck := CacheKey{SST: sst, Method: &method} + + // Store enough items that at least 2 shards get items + storedNames := make(map[string]bool) + for i := range 30 { + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + item.Metadata.SourceName = sst.SourceName + + name := fmt.Sprintf("cross-shard-%d", i) + attrs := make(map[string]any) + attrs["name"] = name + attributes, _ := sdp.ToAttributes(attrs) + item.Attributes = attributes + + cache.StoreItem(ctx, item, 10*time.Second, ck) + storedNames[name] = true + } + + // Count items per shard + shardsWithItems := 0 + for _, shard := range cache.shards { + items, err := shard.Search(ctx, ck) + if err == nil && len(items) > 0 { + shardsWithItems++ + } + } + + if shardsWithItems < 2 { + t.Errorf("expected items on at least 2 shards, got %d", shardsWithItems) + } + + // LIST fan-out should return all items regardless of shard + items, err := cache.searchAll(ctx, ck) + if err != nil { + t.Fatalf("searchAll failed: %v", err) + } + if len(items) != 30 { + t.Errorf("expected 30 items from fan-out, got %d", len(items)) + } +} + +func TestShardedCachePendingWorkDeduplication(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + cache, err := NewShardedCache(dir, DefaultShardCount) + if err != nil { + t.Fatalf("failed to create ShardedCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + ctx := t.Context() + sst := SST{SourceName: "dedup-test", Scope: "scope", Type: "type"} + method := sdp.QueryMethod_LIST + + 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)) + } + } +} + +func TestShardedCacheCloseAndDestroy(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + cache, err := NewShardedCache(dir, DefaultShardCount) + if err != nil { + t.Fatalf("failed to create ShardedCache: %v", err) + } + + ctx := t.Context() + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + cache.StoreItem(ctx, item, 10*time.Second, ck) + + // Verify shard files exist + entries, err := os.ReadDir(dir) + if err != nil { + t.Fatalf("failed to read shard directory: %v", err) + } + if len(entries) != DefaultShardCount { + t.Errorf("expected %d shard files, got %d", DefaultShardCount, len(entries)) + } + + // Close and destroy + if err := cache.CloseAndDestroy(); err != nil { + t.Fatalf("CloseAndDestroy failed: %v", err) + } + + // Verify the directory is removed + if _, err := os.Stat(dir); !os.IsNotExist(err) { + t.Error("shard directory should be removed after CloseAndDestroy") + } +} + +func BenchmarkShardedCacheVsSingleBoltCache(b *testing.B) { + implementations := []struct { + name string + factory func(b *testing.B) Cache + }{ + {"BoltCache", func(b *testing.B) Cache { + c, err := NewBoltCache(filepath.Join(b.TempDir(), "cache.db")) + if err != nil { + b.Fatalf("failed to create BoltCache: %v", err) + } + b.Cleanup(func() { _ = c.CloseAndDestroy() }) + return c + }}, + {"ShardedCache", func(b *testing.B) Cache { + c, err := NewShardedCache( + filepath.Join(b.TempDir(), "shards"), + DefaultShardCount, + ) + if err != nil { + b.Fatalf("failed to create ShardedCache: %v", err) + } + b.Cleanup(func() { _ = c.CloseAndDestroy() }) + return c + }}, + } + + for _, impl := range implementations { + b.Run(impl.name+"/ConcurrentWrite", func(b *testing.B) { + cache := impl.factory(b) + ctx := context.Background() + + sst := SST{SourceName: "bench", Scope: "scope", Type: "type"} + method := sdp.QueryMethod_LIST + ck := CacheKey{SST: sst, Method: &method} + + b.ResetTimer() + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + item.Metadata.SourceName = sst.SourceName + cache.StoreItem(ctx, item, 10*time.Second, ck) + } + }) + }) + } +} + +func TestShardedCacheShardForDeterminism(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + cache, err := NewShardedCache(dir, DefaultShardCount) + if err != nil { + t.Fatalf("failed to create ShardedCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + sst := SST{SourceName: "test", Scope: "scope", Type: "type"} + sstHash := sst.Hash() + + // Same input should always produce the same shard + for range 100 { + idx1 := cache.shardFor(sstHash, "my-unique-value") + idx2 := cache.shardFor(sstHash, "my-unique-value") + if idx1 != idx2 { + t.Fatalf("shardFor is not deterministic: got %d and %d", idx1, idx2) + } + } + + // Different UAVs should produce different shards (at least some of the time) + shardsSeen := make(map[int]bool) + for i := range 100 { + idx := cache.shardFor(sstHash, fmt.Sprintf("value-%d", i)) + shardsSeen[idx] = true + } + if len(shardsSeen) < 2 { + t.Error("expected different UAVs to hash to different shards") + } +} + +func TestShardedCacheErrorRouting(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + cache, err := NewShardedCache(dir, DefaultShardCount) + if err != nil { + t.Fatalf("failed to create ShardedCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + ctx := t.Context() + + t.Run("GET error routes to same shard as GET lookup", func(t *testing.T) { + sst := SST{SourceName: "err-test", Scope: "scope", Type: "type"} + method := sdp.QueryMethod_GET + uav := "my-item" + ck := CacheKey{SST: sst, Method: &method, UniqueAttributeValue: &uav} + + qErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: "not found", + } + cache.StoreError(ctx, qErr, 10*time.Second, ck) + + // Lookup should find the error + hit, _, _, returnedErr, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, uav, false) + defer done() + if !hit { + t.Fatal("expected cache hit for stored error") + } + if returnedErr == nil { + t.Fatal("expected error to be returned") + } + if returnedErr.GetErrorType() != sdp.QueryError_NOTFOUND { + t.Errorf("expected NOTFOUND, got %v", returnedErr.GetErrorType()) + } + }) + + t.Run("LIST error routes to shard 0 and is found via fan-out", func(t *testing.T) { + sst := SST{SourceName: "list-err-test", Scope: "scope", Type: "type"} + method := sdp.QueryMethod_LIST + ck := CacheKey{SST: sst, Method: &method} + + qErr := &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "list failed", + } + cache.StoreError(ctx, qErr, 10*time.Second, ck) + + // LIST lookup fans out, should find the error on shard 0 + hit, _, _, returnedErr, done := cache.Lookup(ctx, sst.SourceName, method, sst.Scope, sst.Type, "", false) + defer done() + if !hit { + t.Fatal("expected cache hit for stored LIST error") + } + if returnedErr == nil { + t.Fatal("expected error to be returned") + } + if returnedErr.GetErrorType() != sdp.QueryError_OTHER { + t.Errorf("expected OTHER, got %v", returnedErr.GetErrorType()) + } + }) +} + +func TestShardedCacheNewCacheFallback(t *testing.T) { + ctx := t.Context() + cache := NewCache(ctx) + + if cache == nil { + t.Fatal("NewCache returned nil") + } + + // Should be a ShardedCache in normal operation + if _, ok := cache.(*ShardedCache); !ok { + t.Logf("NewCache returned %T (may be MemoryCache if ShardedCache creation failed)", cache) + } + + // Basic operation test + item := GenerateRandomItem() + ck := CacheKeyFromQuery(item.GetMetadata().GetSourceQuery(), item.GetMetadata().GetSourceName()) + cache.StoreItem(ctx, item, 10*time.Second, ck) + + hit, _, items, qErr, done := cache.Lookup(ctx, + item.GetMetadata().GetSourceName(), + sdp.QueryMethod_GET, + item.GetScope(), + item.GetType(), + item.UniqueAttributeValue(), + false, + ) + defer done() + if qErr != nil { + t.Fatalf("unexpected error: %v", qErr) + } + if !hit { + t.Fatal("expected cache hit") + } + if len(items) != 1 { + t.Fatalf("expected 1 item, got %d", len(items)) + } +} + +func TestShardedCacheCompactThresholdScaling(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + + parentThreshold := int64(1 * 1024 * 1024 * 1024) // 1GB + perShardThreshold := parentThreshold / int64(DefaultShardCount) + + cache, err := NewShardedCache(dir, DefaultShardCount, + WithCompactThreshold(perShardThreshold), + ) + if err != nil { + t.Fatalf("failed to create ShardedCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + expectedPerShard := parentThreshold / int64(DefaultShardCount) + for i, shard := range cache.shards { + if shard.CompactThreshold != expectedPerShard { + t.Errorf("shard %d: expected CompactThreshold %d, got %d", i, expectedPerShard, shard.CompactThreshold) + } + } +} + +func TestShardedCacheInvalidShardCount(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + + _, err := NewShardedCache(dir, 0) + if err == nil { + t.Error("expected error for shard count 0") + } + + _, err = NewShardedCache(dir, -1) + if err == nil { + t.Error("expected error for negative shard count") + } +} + +func TestShardedCacheConcurrentWriteThroughput(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + cache, err := NewShardedCache(dir, DefaultShardCount) + if err != nil { + t.Fatalf("failed to create ShardedCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + ctx := t.Context() + sst := SST{SourceName: "concurrent", Scope: "scope", Type: "type"} + method := sdp.QueryMethod_LIST + ck := CacheKey{SST: sst, Method: &method} + + var wg sync.WaitGroup + numParallel := 100 + + for i := range numParallel { + idx := i + wg.Go(func() { + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + item.Metadata.SourceName = sst.SourceName + + attrs := make(map[string]any) + attrs["name"] = fmt.Sprintf("concurrent-item-%d", idx) + attributes, _ := sdp.ToAttributes(attrs) + item.Attributes = attributes + + cache.StoreItem(ctx, item, 10*time.Second, ck) + }) + } + + wg.Wait() + + items, searchErr := cache.searchAll(ctx, ck) + if searchErr != nil { + t.Fatalf("searchAll failed: %v", searchErr) + } + if len(items) != numParallel { + t.Errorf("expected %d items, got %d", numParallel, len(items)) + } +} + +func TestShardedCachePurgeAggregation(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + cache, err := NewShardedCache(dir, 3) // Small count for easier verification + if err != nil { + t.Fatalf("failed to create ShardedCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + ctx := t.Context() + sst := SST{SourceName: "purge", Scope: "scope", Type: "type"} + method := sdp.QueryMethod_LIST + ck := CacheKey{SST: sst, Method: &method} + + // Store items with short expiry + for range 10 { + item := GenerateRandomItem() + item.Scope = sst.Scope + item.Type = sst.Type + item.Metadata.SourceName = sst.SourceName + cache.StoreItem(ctx, item, 100*time.Millisecond, ck) + } + + // Wait for expiry + time.Sleep(200 * time.Millisecond) + + // Purge and check aggregated stats + stats := cache.Purge(ctx, time.Now()) + if stats.NumPurged != 10 { + t.Errorf("expected 10 items purged, got %d", stats.NumPurged) + } +} + +// TestShardedCacheShardForBounds verifies that shardFor always returns a valid +// index in [0, shardCount). +func TestShardedCacheShardForBounds(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + cache, err := NewShardedCache(dir, DefaultShardCount) + if err != nil { + t.Fatalf("failed to create ShardedCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + for i := range 10000 { + idx := cache.shardFor(SSTHash(fmt.Sprintf("hash-%d", i)), fmt.Sprintf("uav-%d", i)) + if idx < 0 || idx >= DefaultShardCount { + t.Fatalf("shardFor returned out-of-bounds index %d for shard count %d", idx, DefaultShardCount) + } + } +} + +// TestShardedCacheFNV32aOverflow verifies that the FNV-32a hash mod operation +// works correctly with uint32 values close to math.MaxUint32. +func TestShardedCacheFNV32aOverflow(t *testing.T) { + dir := filepath.Join(t.TempDir(), "shards") + cache, err := NewShardedCache(dir, DefaultShardCount) + if err != nil { + t.Fatalf("failed to create ShardedCache: %v", err) + } + defer func() { _ = cache.CloseAndDestroy() }() + + // These are just strings; the test verifies no panic from the modulo arithmetic + _ = cache.shardFor(SSTHash(fmt.Sprintf("%d", math.MaxUint32)), "test") + _ = cache.shardFor(SSTHash(""), "") + _ = cache.shardFor(SSTHash("a"), "b") +} diff --git a/sources/gcp/manual/cloud-kms-crypto-key_test.go b/sources/gcp/manual/cloud-kms-crypto-key_test.go index b2e98b7b..6790ba57 100644 --- a/sources/gcp/manual/cloud-kms-crypto-key_test.go +++ b/sources/gcp/manual/cloud-kms-crypto-key_test.go @@ -344,13 +344,19 @@ func TestCloudKMSCryptoKey(t *testing.T) { t.Fatalf("Expected 2 items with legacy format, got: %d", len(items)) } - // Verify the returned items have the correct unique attributes - uniqueAttr1, err := items[0].GetAttributes().Get("uniqueAttr") - if err != nil { - t.Fatalf("Failed to get uniqueAttr from item 1: %v", err) - } - if uniqueAttr1 != "us-central1|my-keyring|my-key-1" { - t.Fatalf("Expected uniqueAttr 'us-central1|my-keyring|my-key-1', got: %v", uniqueAttr1) + // Verify both expected items are present (order is not guaranteed) + found := make(map[string]bool) + for _, item := range items { + ua, err := item.GetAttributes().Get("uniqueAttr") + if err != nil { + t.Fatalf("Failed to get uniqueAttr: %v", err) + } + found[ua.(string)] = true + } + for _, expected := range []string{"us-central1|my-keyring|my-key-1", "us-central1|my-keyring|my-key-2"} { + if !found[expected] { + t.Fatalf("Expected item with uniqueAttr %q not found in results", expected) + } } }) From fd808ca350b028c866f6be856f60d287e4845d45 Mon Sep 17 00:00:00 2001 From: Lionel Wilson <80872669+Lionel-Wilson@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:25:39 +0000 Subject: [PATCH 68/74] Implement NAT Gateways Client and Adapter for Azure (#4169) image > [!NOTE] > **Medium Risk** > Medium risk because it adds a new Azure SDK client and adapter into the main `Adapters()` initialization path, increasing API surface/calls and requiring correct scope/link parsing. Changes are additive and covered by unit tests/mocks. > > **Overview** > **Adds discovery for Azure NAT Gateways.** Introduces `clients.NatGatewaysClient` (with list pager support) plus a generated GoMock, and registers a new `NewNetworkNatGateway` wrapper/adapter. > > The NAT gateway wrapper supports `Get`, `List`, and `ListStream`, maps provisioning state to item health, and emits linked-item queries to related `PublicIPAddress`, `PublicIPPrefix`, `Subnet`, and `VirtualNetwork` resources; `manual/adapters.go` now initializes `armnetwork.NewNatGatewaysClient` and includes the adapter in both real and placeholder adapter lists, with dedicated unit tests validating get/list behavior and link generation. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit d470996177d2dd3f107bcdd78f0549ca2e2bd3dd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 968c00193a187dfd2fcd95ecf245cac53afb2c20 --- sources/azure/clients/nat-gateways-client.go | 35 ++ sources/azure/manual/adapters.go | 10 + sources/azure/manual/network-nat-gateway.go | 283 ++++++++++++++ .../azure/manual/network-nat-gateway_test.go | 354 ++++++++++++++++++ .../shared/mocks/mock_nat_gateways_client.go | 72 ++++ 5 files changed, 754 insertions(+) create mode 100644 sources/azure/clients/nat-gateways-client.go create mode 100644 sources/azure/manual/network-nat-gateway.go create mode 100644 sources/azure/manual/network-nat-gateway_test.go create mode 100644 sources/azure/shared/mocks/mock_nat_gateways_client.go diff --git a/sources/azure/clients/nat-gateways-client.go b/sources/azure/clients/nat-gateways-client.go new file mode 100644 index 00000000..635f50ed --- /dev/null +++ b/sources/azure/clients/nat-gateways-client.go @@ -0,0 +1,35 @@ +package clients + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" +) + +//go:generate mockgen -destination=../shared/mocks/mock_nat_gateways_client.go -package=mocks -source=nat-gateways-client.go + +// NatGatewaysPager is a type alias for the generic Pager interface with NAT gateway list response type. +type NatGatewaysPager = Pager[armnetwork.NatGatewaysClientListResponse] + +// NatGatewaysClient is an interface for interacting with Azure NAT gateways. +type NatGatewaysClient interface { + Get(ctx context.Context, resourceGroupName string, natGatewayName string, options *armnetwork.NatGatewaysClientGetOptions) (armnetwork.NatGatewaysClientGetResponse, error) + NewListPager(resourceGroupName string, options *armnetwork.NatGatewaysClientListOptions) NatGatewaysPager +} + +type natGatewaysClient struct { + client *armnetwork.NatGatewaysClient +} + +func (c *natGatewaysClient) Get(ctx context.Context, resourceGroupName string, natGatewayName string, options *armnetwork.NatGatewaysClientGetOptions) (armnetwork.NatGatewaysClientGetResponse, error) { + return c.client.Get(ctx, resourceGroupName, natGatewayName, options) +} + +func (c *natGatewaysClient) NewListPager(resourceGroupName string, options *armnetwork.NatGatewaysClientListOptions) NatGatewaysPager { + return c.client.NewListPager(resourceGroupName, options) +} + +// NewNatGatewaysClient creates a new NatGatewaysClient from the Azure SDK client. +func NewNatGatewaysClient(client *armnetwork.NatGatewaysClient) NatGatewaysClient { + return &natGatewaysClient{client: client} +} diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 227d8371..47c9f0f2 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -251,6 +251,11 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred return nil, fmt.Errorf("failed to create virtual network gateways client: %w", err) } + natGatewaysClient, err := armnetwork.NewNatGatewaysClient(subscriptionID, cred, nil) + if err != nil { + return nil, fmt.Errorf("failed to create nat gateways client: %w", err) + } + managedHSMsClient, err := armkeyvault.NewManagedHsmsClient(subscriptionID, cred, nil) if err != nil { return nil, fmt.Errorf("failed to create managed hsms client: %w", err) @@ -591,6 +596,10 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred clients.NewVirtualNetworkGatewaysClient(virtualNetworkGatewaysClient), resourceGroupScopes, ), cache), + sources.WrapperToAdapter(NewNetworkNatGateway( + clients.NewNatGatewaysClient(natGatewaysClient), + resourceGroupScopes, + ), cache), sources.WrapperToAdapter(NewSqlServer( clients.NewSqlServersClient(sqlServersClient), resourceGroupScopes, @@ -747,6 +756,7 @@ func Adapters(ctx context.Context, subscriptionID string, regions []string, cred sources.WrapperToAdapter(NewNetworkRouteTable(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkApplicationGateway(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewNetworkVirtualNetworkGateway(nil, placeholderResourceGroupScopes), noOpCache), + sources.WrapperToAdapter(NewNetworkNatGateway(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewSqlServer(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServer(nil, placeholderResourceGroupScopes), noOpCache), sources.WrapperToAdapter(NewDBforPostgreSQLFlexibleServerFirewallRule(nil, placeholderResourceGroupScopes), noOpCache), diff --git a/sources/azure/manual/network-nat-gateway.go b/sources/azure/manual/network-nat-gateway.go new file mode 100644 index 00000000..e6f6f98f --- /dev/null +++ b/sources/azure/manual/network-nat-gateway.go @@ -0,0 +1,283 @@ +package manual + +import ( + "context" + "errors" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "github.com/overmindtech/cli/go/discovery" + "github.com/overmindtech/cli/go/sdp-go" + "github.com/overmindtech/cli/go/sdpcache" + "github.com/overmindtech/cli/sources" + "github.com/overmindtech/cli/sources/azure/clients" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/shared" +) + +var NetworkNatGatewayLookupByName = shared.NewItemTypeLookup("name", azureshared.NetworkNatGateway) + +type networkNatGatewayWrapper struct { + client clients.NatGatewaysClient + + *azureshared.MultiResourceGroupBase +} + +// NewNetworkNatGateway creates a new networkNatGatewayWrapper instance. +func NewNetworkNatGateway(client clients.NatGatewaysClient, resourceGroupScopes []azureshared.ResourceGroupScope) sources.ListableWrapper { + return &networkNatGatewayWrapper{ + client: client, + MultiResourceGroupBase: azureshared.NewMultiResourceGroupBase( + resourceGroupScopes, + sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + azureshared.NetworkNatGateway, + ), + } +} + +func (n networkNatGatewayWrapper) List(ctx context.Context, scope string) ([]*sdp.Item, *sdp.QueryError) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + pager := n.client.NewListPager(rgScope.ResourceGroup, nil) + + var items []*sdp.Item + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + for _, ng := range page.Value { + if ng.Name == nil { + continue + } + item, sdpErr := n.azureNatGatewayToSDPItem(ng, scope) + if sdpErr != nil { + return nil, sdpErr + } + items = append(items, item) + } + } + + return items, nil +} + +func (n networkNatGatewayWrapper) ListStream(ctx context.Context, stream discovery.QueryResultStream, cache sdpcache.Cache, cacheKey sdpcache.CacheKey, scope string) { + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + pager := n.client.NewListPager(rgScope.ResourceGroup, nil) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + stream.SendError(azureshared.QueryError(err, scope, n.Type())) + return + } + + for _, ng := range page.Value { + if ng.Name == nil { + continue + } + item, sdpErr := n.azureNatGatewayToSDPItem(ng, scope) + if sdpErr != nil { + stream.SendError(sdpErr) + continue + } + cache.StoreItem(ctx, item, shared.DefaultCacheDuration, cacheKey) + stream.SendItem(item) + } + } +} + +func (n networkNatGatewayWrapper) Get(ctx context.Context, scope string, queryParts ...string) (*sdp.Item, *sdp.QueryError) { + if len(queryParts) < 1 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_OTHER, + ErrorString: "Get requires 1 query part: natGatewayName", + Scope: scope, + ItemType: n.Type(), + } + } + + natGatewayName := queryParts[0] + + rgScope, err := n.ResourceGroupScopeFromScope(scope) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + resp, err := n.client.Get(ctx, rgScope.ResourceGroup, natGatewayName, nil) + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + return n.azureNatGatewayToSDPItem(&resp.NatGateway, scope) +} + +func (n networkNatGatewayWrapper) azureNatGatewayToSDPItem(ng *armnetwork.NatGateway, scope string) (*sdp.Item, *sdp.QueryError) { + attributes, err := shared.ToAttributesWithExclude(ng, "tags") + if err != nil { + return nil, azureshared.QueryError(err, scope, n.Type()) + } + + if ng.Name == nil { + return nil, azureshared.QueryError(errors.New("nat gateway name is nil"), scope, n.Type()) + } + + sdpItem := &sdp.Item{ + Type: azureshared.NetworkNatGateway.String(), + UniqueAttribute: "name", + Attributes: attributes, + Scope: scope, + Tags: azureshared.ConvertAzureTags(ng.Tags), + LinkedItemQueries: []*sdp.LinkedItemQuery{}, + } + + // Health from provisioning state + if ng.Properties != nil && ng.Properties.ProvisioningState != nil { + switch *ng.Properties.ProvisioningState { + case armnetwork.ProvisioningStateSucceeded: + sdpItem.Health = sdp.Health_HEALTH_OK.Enum() + case armnetwork.ProvisioningStateCreating, armnetwork.ProvisioningStateUpdating, armnetwork.ProvisioningStateDeleting: + sdpItem.Health = sdp.Health_HEALTH_PENDING.Enum() + case armnetwork.ProvisioningStateFailed, armnetwork.ProvisioningStateCanceled: + sdpItem.Health = sdp.Health_HEALTH_ERROR.Enum() + default: + sdpItem.Health = sdp.Health_HEALTH_UNKNOWN.Enum() + } + } + + // Linked resources from Properties + if ng.Properties == nil { + return sdpItem, nil + } + props := ng.Properties + + // Public IP addresses (V4 and V6) + for _, refs := range [][]*armnetwork.SubResource{props.PublicIPAddresses, props.PublicIPAddressesV6} { + for _, ref := range refs { + if ref != nil && ref.ID != nil { + refID := *ref.ID + refName := azureshared.ExtractResourceName(refID) + if refName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(refID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPublicIPAddress.String(), + Method: sdp.QueryMethod_GET, + Query: refName, + Scope: linkedScope, + }, + }) + } + } + } + } + + // Public IP prefixes (V4 and V6) + for _, refs := range [][]*armnetwork.SubResource{props.PublicIPPrefixes, props.PublicIPPrefixesV6} { + for _, ref := range refs { + if ref != nil && ref.ID != nil { + refID := *ref.ID + refName := azureshared.ExtractResourceName(refID) + if refName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(refID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkPublicIPPrefix.String(), + Method: sdp.QueryMethod_GET, + Query: refName, + Scope: linkedScope, + }, + }) + } + } + } + } + + // Subnets (read-only references: subnets using this NAT gateway) + for _, ref := range props.Subnets { + if ref != nil && ref.ID != nil { + subnetID := *ref.ID + params := azureshared.ExtractPathParamsFromResourceID(subnetID, []string{"virtualNetworks", "subnets"}) + if len(params) >= 2 && params[0] != "" && params[1] != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(subnetID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkSubnet.String(), + Method: sdp.QueryMethod_GET, + Scope: linkedScope, + Query: shared.CompositeLookupKey(params[0], params[1]), + }, + }) + } + } + } + + // Source virtual network + if props.SourceVirtualNetwork != nil && props.SourceVirtualNetwork.ID != nil { + vnetID := *props.SourceVirtualNetwork.ID + vnetName := azureshared.ExtractResourceName(vnetID) + if vnetName != "" { + linkedScope := azureshared.ExtractScopeFromResourceID(vnetID) + if linkedScope == "" { + linkedScope = scope + } + sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: azureshared.NetworkVirtualNetwork.String(), + Method: sdp.QueryMethod_GET, + Query: vnetName, + Scope: linkedScope, + }, + }) + } + } + + return sdpItem, nil +} + +func (n networkNatGatewayWrapper) GetLookups() sources.ItemTypeLookups { + return sources.ItemTypeLookups{ + NetworkNatGatewayLookupByName, + } +} + +func (n networkNatGatewayWrapper) PotentialLinks() map[shared.ItemType]bool { + return map[shared.ItemType]bool{ + azureshared.NetworkPublicIPAddress: true, + azureshared.NetworkPublicIPPrefix: true, + azureshared.NetworkSubnet: true, + azureshared.NetworkVirtualNetwork: true, + } +} + +func (n networkNatGatewayWrapper) TerraformMappings() []*sdp.TerraformMapping { + return []*sdp.TerraformMapping{ + { + TerraformMethod: sdp.QueryMethod_GET, + TerraformQueryMap: "azurerm_nat_gateway.name", + }, + } +} + +func (n networkNatGatewayWrapper) IAMPermissions() []string { + return []string{ + "Microsoft.Network/natGateways/read", + } +} + +func (n networkNatGatewayWrapper) PredefinedRole() string { + return "Reader" +} diff --git a/sources/azure/manual/network-nat-gateway_test.go b/sources/azure/manual/network-nat-gateway_test.go new file mode 100644 index 00000000..79fac9e8 --- /dev/null +++ b/sources/azure/manual/network-nat-gateway_test.go @@ -0,0 +1,354 @@ +package manual_test + +import ( + "context" + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + "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/manual" + azureshared "github.com/overmindtech/cli/sources/azure/shared" + "github.com/overmindtech/cli/sources/azure/shared/mocks" + "github.com/overmindtech/cli/sources/shared" +) + +func TestNetworkNatGateway(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) { + natGatewayName := "test-nat-gateway" + ng := createAzureNatGateway(natGatewayName) + + mockClient := mocks.NewMockNatGatewaysClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, natGatewayName, nil).Return( + armnetwork.NatGatewaysClientGetResponse{ + NatGateway: *ng, + }, nil) + + wrapper := manual.NewNetworkNatGateway(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, natGatewayName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + if sdpItem.GetType() != azureshared.NetworkNatGateway.String() { + t.Errorf("Expected type %s, got %s", azureshared.NetworkNatGateway.String(), sdpItem.GetType()) + } + + if sdpItem.GetUniqueAttribute() != "name" { + t.Errorf("Expected unique attribute 'name', got %s", sdpItem.GetUniqueAttribute()) + } + + if sdpItem.UniqueAttributeValue() != natGatewayName { + t.Errorf("Expected unique attribute value %s, got %s", natGatewayName, 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("Get_WithLinkedResources", func(t *testing.T) { + natGatewayName := "test-nat-gateway-with-links" + ng := createAzureNatGatewayWithLinks(natGatewayName, subscriptionID, resourceGroup) + + mockClient := mocks.NewMockNatGatewaysClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, natGatewayName, nil).Return( + armnetwork.NatGatewaysClientGetResponse{ + NatGateway: *ng, + }, nil) + + wrapper := manual.NewNetworkNatGateway(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + sdpItem, qErr := adapter.Get(ctx, scope, natGatewayName, true) + if qErr != nil { + t.Fatalf("Expected no error, got: %v", qErr) + } + + t.Run("StaticTests", func(t *testing.T) { + queryTests := shared.QueryTests{ + { + ExpectedType: azureshared.NetworkPublicIPAddress.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-public-ip", + ExpectedScope: scope, + }, + { + ExpectedType: azureshared.NetworkPublicIPPrefix.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "test-public-ip-prefix", + ExpectedScope: scope, + }, + { + ExpectedType: azureshared.NetworkSubnet.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: shared.CompositeLookupKey("test-vnet", "test-subnet"), + ExpectedScope: scope, + }, + { + ExpectedType: azureshared.NetworkVirtualNetwork.String(), + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "source-vnet", + ExpectedScope: scope, + }, + } + shared.RunStaticTests(t, adapter, sdpItem, queryTests) + }) + }) + + t.Run("GetWithEmptyName", func(t *testing.T) { + mockClient := mocks.NewMockNatGatewaysClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, "", nil).Return( + armnetwork.NatGatewaysClientGetResponse{}, errors.New("nat gateway not found")) + + wrapper := manual.NewNetworkNatGateway(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 nat gateway with empty name, but got nil") + } + }) + + t.Run("ErrorHandling", func(t *testing.T) { + natGatewayName := "nonexistent-nat-gateway" + expectedErr := errors.New("nat gateway not found") + + mockClient := mocks.NewMockNatGatewaysClient(ctrl) + mockClient.EXPECT().Get(ctx, resourceGroup, natGatewayName, nil).Return( + armnetwork.NatGatewaysClientGetResponse{}, expectedErr) + + wrapper := manual.NewNetworkNatGateway(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + _, qErr := adapter.Get(ctx, scope, natGatewayName, true) + if qErr == nil { + t.Fatal("Expected error when nat gateway not found, got nil") + } + }) + + t.Run("List", func(t *testing.T) { + ng1 := createAzureNatGateway("nat-gateway-1") + ng2 := createAzureNatGateway("nat-gateway-2") + + mockClient := mocks.NewMockNatGatewaysClient(ctrl) + mockPager := newMockNatGatewaysPager(ctrl, []*armnetwork.NatGateway{ng1, ng2}) + + mockClient.EXPECT().NewListPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkNatGateway(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") + } + + items, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(items) != 2 { + t.Fatalf("Expected 2 items, got %d", len(items)) + } + + for i, item := range items { + if item.GetType() != azureshared.NetworkNatGateway.String() { + t.Errorf("Item %d: expected type %s, got %s", i, azureshared.NetworkNatGateway.String(), item.GetType()) + } + if item.Validate() != nil { + t.Errorf("Item %d: validation error: %v", i, item.Validate()) + } + } + }) + + t.Run("ListStream", func(t *testing.T) { + ng1 := createAzureNatGateway("nat-gateway-1") + ng2 := createAzureNatGateway("nat-gateway-2") + + mockClient := mocks.NewMockNatGatewaysClient(ctrl) + mockPager := newMockNatGatewaysPager(ctrl, []*armnetwork.NatGateway{ng1, ng2}) + + mockClient.EXPECT().NewListPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkNatGateway(mockClient, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + adapter := sources.WrapperToAdapter(wrapper, sdpcache.NewNoOpCache()) + + listStream, ok := adapter.(discovery.ListStreamableAdapter) + if !ok { + t.Fatalf("Adapter does not support ListStream operation") + } + + var received []*sdp.Item + stream := &collectingStream{items: &received} + listStream.ListStream(ctx, scope, true, stream) + + if len(received) != 2 { + t.Fatalf("Expected 2 items from stream, got %d", len(received)) + } + }) + + t.Run("List_NilNameSkipped", func(t *testing.T) { + ng1 := createAzureNatGateway("nat-gateway-1") + ng2NilName := createAzureNatGateway("nat-gateway-2") + ng2NilName.Name = nil + + mockClient := mocks.NewMockNatGatewaysClient(ctrl) + mockPager := newMockNatGatewaysPager(ctrl, []*armnetwork.NatGateway{ng1, ng2NilName}) + + mockClient.EXPECT().NewListPager(resourceGroup, nil).Return(mockPager) + + wrapper := manual.NewNetworkNatGateway(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") + } + + items, err := listable.List(ctx, scope, true) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(items) != 1 { + t.Fatalf("Expected 1 item (nil name skipped), got %d", len(items)) + } + if items[0].UniqueAttributeValue() != "nat-gateway-1" { + t.Errorf("Expected only nat-gateway-1, got %s", items[0].UniqueAttributeValue()) + } + }) + + t.Run("GetLookups", func(t *testing.T) { + wrapper := manual.NewNetworkNatGateway(nil, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + lookups := wrapper.GetLookups() + if len(lookups) == 0 { + t.Error("Expected GetLookups to return at least one lookup") + } + found := false + for _, l := range lookups { + if l.ItemType.String() == azureshared.NetworkNatGateway.String() { + found = true + break + } + } + if !found { + t.Error("Expected GetLookups to include NetworkNatGateway") + } + }) + + t.Run("PotentialLinks", func(t *testing.T) { + wrapper := manual.NewNetworkNatGateway(nil, []azureshared.ResourceGroupScope{azureshared.NewResourceGroupScope(subscriptionID, resourceGroup)}) + potentialLinks := wrapper.PotentialLinks() + for _, linkType := range []shared.ItemType{ + azureshared.NetworkPublicIPAddress, + azureshared.NetworkPublicIPPrefix, + azureshared.NetworkSubnet, + azureshared.NetworkVirtualNetwork, + } { + if !potentialLinks[linkType] { + t.Errorf("Expected PotentialLinks to include %s", linkType) + } + } + }) +} + +type mockNatGatewaysPager struct { + ctrl *gomock.Controller + items []*armnetwork.NatGateway + index int + more bool +} + +func newMockNatGatewaysPager(ctrl *gomock.Controller, items []*armnetwork.NatGateway) *mockNatGatewaysPager { + return &mockNatGatewaysPager{ + ctrl: ctrl, + items: items, + index: 0, + more: len(items) > 0, + } +} + +func (m *mockNatGatewaysPager) More() bool { + return m.more +} + +func (m *mockNatGatewaysPager) NextPage(ctx context.Context) (armnetwork.NatGatewaysClientListResponse, error) { + if m.index >= len(m.items) { + m.more = false + return armnetwork.NatGatewaysClientListResponse{ + NatGatewayListResult: armnetwork.NatGatewayListResult{ + Value: []*armnetwork.NatGateway{}, + }, + }, nil + } + item := m.items[m.index] + m.index++ + m.more = m.index < len(m.items) + return armnetwork.NatGatewaysClientListResponse{ + NatGatewayListResult: armnetwork.NatGatewayListResult{ + Value: []*armnetwork.NatGateway{item}, + }, + }, nil +} + +func createAzureNatGateway(name string) *armnetwork.NatGateway { + provisioningState := armnetwork.ProvisioningStateSucceeded + return &armnetwork.NatGateway{ + ID: new("/subscriptions/test-sub/resourceGroups/test-rg/providers/Microsoft.Network/natGateways/" + name), + Name: new(name), + Type: new("Microsoft.Network/natGateways"), + Location: new("eastus"), + Tags: map[string]*string{ + "env": new("test"), + "project": new("testing"), + }, + Properties: &armnetwork.NatGatewayPropertiesFormat{ + ProvisioningState: &provisioningState, + }, + } +} + +func createAzureNatGatewayWithLinks(name, subscriptionID, resourceGroup string) *armnetwork.NatGateway { + ng := createAzureNatGateway(name) + baseID := "/subscriptions/" + subscriptionID + "/resourceGroups/" + resourceGroup + "/providers/Microsoft.Network" + publicIPID := baseID + "/publicIPAddresses/test-public-ip" + publicIPPrefixID := baseID + "/publicIPPrefixes/test-public-ip-prefix" + subnetID := baseID + "/virtualNetworks/test-vnet/subnets/test-subnet" + sourceVnetID := baseID + "/virtualNetworks/source-vnet" + + ng.Properties.PublicIPAddresses = []*armnetwork.SubResource{ + {ID: new(publicIPID)}, + } + ng.Properties.PublicIPPrefixes = []*armnetwork.SubResource{ + {ID: new(publicIPPrefixID)}, + } + ng.Properties.Subnets = []*armnetwork.SubResource{ + {ID: new(subnetID)}, + } + ng.Properties.SourceVirtualNetwork = &armnetwork.SubResource{ + ID: new(sourceVnetID), + } + return ng +} diff --git a/sources/azure/shared/mocks/mock_nat_gateways_client.go b/sources/azure/shared/mocks/mock_nat_gateways_client.go new file mode 100644 index 00000000..2c53c0e2 --- /dev/null +++ b/sources/azure/shared/mocks/mock_nat_gateways_client.go @@ -0,0 +1,72 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: nat-gateways-client.go +// +// Generated by this command: +// +// mockgen -destination=../shared/mocks/mock_nat_gateways_client.go -package=mocks -source=nat-gateways-client.go +// + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + armnetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9" + clients "github.com/overmindtech/cli/sources/azure/clients" + gomock "go.uber.org/mock/gomock" +) + +// MockNatGatewaysClient is a mock of NatGatewaysClient interface. +type MockNatGatewaysClient struct { + ctrl *gomock.Controller + recorder *MockNatGatewaysClientMockRecorder + isgomock struct{} +} + +// MockNatGatewaysClientMockRecorder is the mock recorder for MockNatGatewaysClient. +type MockNatGatewaysClientMockRecorder struct { + mock *MockNatGatewaysClient +} + +// NewMockNatGatewaysClient creates a new mock instance. +func NewMockNatGatewaysClient(ctrl *gomock.Controller) *MockNatGatewaysClient { + mock := &MockNatGatewaysClient{ctrl: ctrl} + mock.recorder = &MockNatGatewaysClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockNatGatewaysClient) EXPECT() *MockNatGatewaysClientMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockNatGatewaysClient) Get(ctx context.Context, resourceGroupName, natGatewayName string, options *armnetwork.NatGatewaysClientGetOptions) (armnetwork.NatGatewaysClientGetResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, resourceGroupName, natGatewayName, options) + ret0, _ := ret[0].(armnetwork.NatGatewaysClientGetResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockNatGatewaysClientMockRecorder) Get(ctx, resourceGroupName, natGatewayName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockNatGatewaysClient)(nil).Get), ctx, resourceGroupName, natGatewayName, options) +} + +// NewListPager mocks base method. +func (m *MockNatGatewaysClient) NewListPager(resourceGroupName string, options *armnetwork.NatGatewaysClientListOptions) clients.NatGatewaysPager { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewListPager", resourceGroupName, options) + ret0, _ := ret[0].(clients.NatGatewaysPager) + return ret0 +} + +// NewListPager indicates an expected call of NewListPager. +func (mr *MockNatGatewaysClientMockRecorder) NewListPager(resourceGroupName, options any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewListPager", reflect.TypeOf((*MockNatGatewaysClient)(nil).NewListPager), resourceGroupName, options) +} From 8593a4e4de530e8741ffdb3a5a466c0c76c6a0ab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:06:59 +0000 Subject: [PATCH 69/74] fix(deps): update azure-sdk-for-go monorepo (major) (#4144) 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/) | |---|---|---|---| | [github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3](https://redirect.github.com/Azure/azure-sdk-for-go) | `v3.0.1` → `v4.0.0` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fAzure%2fazure-sdk-for-go%2fsdk%2fresourcemanager%2fbatch%2farmbatch%2fv3/v4.0.0?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fAzure%2fazure-sdk-for-go%2fsdk%2fresourcemanager%2fbatch%2farmbatch%2fv3/v3.0.1/v4.0.0?slim=true) | | [github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2](https://redirect.github.com/Azure/azure-sdk-for-go) | `v2.1.0` → `v3.0.1` | ![age](https://developer.mend.io/api/mc/badges/age/go/github.com%2fAzure%2fazure-sdk-for-go%2fsdk%2fresourcemanager%2fresources%2farmresources%2fv2/v3.0.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/go/github.com%2fAzure%2fazure-sdk-for-go%2fsdk%2fresourcemanager%2fresources%2farmresources%2fv2/v2.1.0/v3.0.1?slim=true) | --- > [!WARNING] > Some dependencies could not be looked up. Check the [Dependency Dashboard](../issues/370) 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. 👻 **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). --- > [!NOTE] > **Medium Risk** > Upgrades a major Azure SDK dependency and adjusts Batch adapters to match removed/changed SDK fields, which could affect Azure Batch discovery and linked-item generation at runtime. > > **Overview** > **Upgrades Azure Batch ARM SDK from `armbatch/v3` to `armbatch/v4`** and updates all Batch account/application/pool clients, manual wrappers, integration tests, and generated GoMock stubs to use the new import path. > > Aligns Batch pool linking behavior with SDK changes by **dropping certificate reference linking** (and removing `BatchBatchCertificate` from pool `PotentialLinks`) because `armbatch/v4` no longer exposes pool certificate refs. > > Refreshes `go.mod`/`go.sum` for the dependency upgrade (including new Azure SDK indirect deps and a `jwt/v5` patch bump). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit c3807becb46015c7cfe621c20848e4d2e6ea6c37. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: lionel.wilson GitOrigin-RevId: 57bdc182de2762672c5aaff70627588886400318 --- go.mod | 9 ++++--- go.sum | 16 +++++++----- .../azure/clients/batch-accounts-client.go | 2 +- .../azure/clients/batch-application-client.go | 2 +- sources/azure/clients/batch-pool-client.go | 2 +- .../batch-batch-accounts_test.go | 2 +- sources/azure/manual/adapters.go | 2 +- sources/azure/manual/batch-batch-accounts.go | 2 +- .../azure/manual/batch-batch-accounts_test.go | 2 +- .../azure/manual/batch-batch-application.go | 2 +- .../manual/batch-batch-application_test.go | 2 +- sources/azure/manual/batch-batch-pool.go | 25 ++----------------- sources/azure/manual/batch-batch-pool_test.go | 5 +--- .../mocks/mock_batch_accounts_client.go | 2 +- .../mocks/mock_batch_application_client.go | 2 +- .../shared/mocks/mock_batch_pool_client.go | 2 +- 16 files changed, 30 insertions(+), 49 deletions(-) diff --git a/go.mod b/go.mod index 1e277e4d..b90fdf83 100644 --- a/go.mod +++ b/go.mod @@ -45,7 +45,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 - github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3 v3.0.1 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4 v4.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.3.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3 v3.4.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 @@ -53,7 +53,9 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v9 v9.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5 v5.0.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2 v2.1.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v3 v3.0.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2 v2.0.0-beta.7 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3 v3.0.0 github.com/Masterminds/semver/v3 v3.4.0 @@ -212,6 +214,7 @@ require ( 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/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.1 // 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 @@ -310,7 +313,7 @@ require ( 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-jwt/jwt/v5 v5.3.1 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/cel-go v0.27.0 // indirect @@ -520,5 +523,3 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.2 sigs.k8s.io/yaml v1.6.0 // indirect ) - -require github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 diff --git a/go.sum b/go.sum index 92e23c81..4da8ec2d 100644 --- a/go.sum +++ b/go.sum @@ -116,16 +116,16 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDo github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 h1:qiir/pptnHqp6hV8QwV+IExYIf6cPsXBfUDUXQ27t2Y= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2/go.mod h1:jVRrRDLCOuif95HDYC23ADTMlvahB7tMdl519m9Iyjc= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3 v3.0.1 h1:6aObZUybvkz7Sm2d/GxgsZ+0hbhA0RC5p+81aAxQ/Po= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3 v3.0.1/go.mod h1:kz6cfDXtcUJWUjLKSlXW+oBqtWovK648UYJDZYtAZ3g= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4 v4.0.0 h1:KBRoKIQlg79mFK5LRndDGPrCDGRl2xyFr/vG8afLGys= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4 v4.0.0/go.mod h1:w+PG/dv/phWHlE3OIKWa4CAITETZ52D8qznRGMbduPA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.3.0 h1:nyxugFxG2uhbMeJVCFFuD2j9wu+6KgeabITdINraQsE= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7 v7.3.0/go.mod h1:e4RAYykLIz73CF52KhSooo4whZGXvXrD09m0jkgnWiU= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3 v3.4.0 h1:+EhRnIOLvffCvUMUfP+MgOp6PrtN1d6xt94DZtrC3lA= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3 v3.4.0/go.mod h1:Bb7kqorvA2acMCNFac+2ldoQWi7QrcMdH+9Gg9C7fSM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= -github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.1 h1:1kpY4qe+BGAH2ykv4baVSqyx+AY5VjXeJ15SldlU6hs= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.1/go.mod h1:nT6cWpWdUt+g81yuKmjeYPUtI73Ak3yQIT4PVVsCEEQ= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2 v2.0.1 h1:nFZ7AvJqTpWobmnZlprsK6GucrByFsXWB+DwkhRxM9I= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault/v2 v2.0.1/go.mod h1:ZNiswYTEPuQ/D+mHxONII+FeHHNNVQlJ5IUG88opjS0= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.2.0 h1:akP6VpxJGgQRpDR1P462piz/8OhYLRCreDj48AyNabc= @@ -138,10 +138,14 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlfl github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/postgresql/armpostgresqlflexibleservers/v5 v5.0.0/go.mod h1:EHRrmrnS2Q8fB3+DE30TTk04JLqjui5ZJEF7eMVQ2/M= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 h1:yzrctSl9GMIQ5lHu7jc8olOsGjWDCsBpJhWqfGa/YIM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0/go.mod h1:GE4m0rnnfwLGX0Y9A9A25Zx5N/90jneT5ABevqzhuFQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeployments v0.2.0 h1:bYq3jfB2x36hslKMHyge3+esWzROtJNk/4dCjsKlrl4= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armdeployments v0.2.0/go.mod h1:fewgRjNVE84QVVh798sIMFb7gPXPp7NmnekGnboSnXk= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2 v2.1.0 h1:seyVIpxalxYmfjoo8MB4rRzWaobMG+KJ2+MAUrEvDGU= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2 v2.1.0/go.mod h1:M3QD7IyKZBaC4uAKjitTOSOXdcPC6JS1A9oOW3hYjbQ= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v3 v3.0.1 h1:guyQA4b8XB2sbJZXzUnOF9mn0WDBv/ZT7me9wTipKtE= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v3 v3.0.1/go.mod h1:8h8yhzh9o+0HeSIhUxYny+rEQajScrfIpNktvgYG3Q8= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2 v2.0.0-beta.7 h1:SLsVdG/8T65poVMw5ZJtI/dUL7iIwvbkq+koqmWdmu8= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql/v2 v2.0.0-beta.7/go.mod h1:l9kSL5eB+KdZ2aovhkUYwyZE7oQwTEqVCxnpNKChi1U= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3 v3.0.0 h1:tqGq5xt/rNU57Eb52rf6bvrNWoKPSwLDVUQrJnF4C5U= @@ -536,8 +540,8 @@ 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-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/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= diff --git a/sources/azure/clients/batch-accounts-client.go b/sources/azure/clients/batch-accounts-client.go index 3c41029f..524ab924 100644 --- a/sources/azure/clients/batch-accounts-client.go +++ b/sources/azure/clients/batch-accounts-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" ) //go:generate mockgen -destination=../shared/mocks/mock_batch_accounts_client.go -package=mocks -source=batch-accounts-client.go diff --git a/sources/azure/clients/batch-application-client.go b/sources/azure/clients/batch-application-client.go index 8b3c5de3..f57ebb6d 100644 --- a/sources/azure/clients/batch-application-client.go +++ b/sources/azure/clients/batch-application-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" ) //go:generate mockgen -destination=../shared/mocks/mock_batch_application_client.go -package=mocks -source=batch-application-client.go diff --git a/sources/azure/clients/batch-pool-client.go b/sources/azure/clients/batch-pool-client.go index 22788075..bc7320b3 100644 --- a/sources/azure/clients/batch-pool-client.go +++ b/sources/azure/clients/batch-pool-client.go @@ -3,7 +3,7 @@ package clients import ( "context" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" ) //go:generate mockgen -destination=../shared/mocks/mock_batch_pool_client.go -package=mocks -source=batch-pool-client.go diff --git a/sources/azure/integration-tests/batch-batch-accounts_test.go b/sources/azure/integration-tests/batch-batch-accounts_test.go index d3836ebe..4a2c02a2 100644 --- a/sources/azure/integration-tests/batch-batch-accounts_test.go +++ b/sources/azure/integration-tests/batch-batch-accounts_test.go @@ -12,7 +12,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3" log "github.com/sirupsen/logrus" diff --git a/sources/azure/manual/adapters.go b/sources/azure/manual/adapters.go index 47c9f0f2..aaeba7cb 100644 --- a/sources/azure/manual/adapters.go +++ b/sources/azure/manual/adapters.go @@ -6,7 +6,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v7" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cosmos/armcosmos/v3" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns" diff --git a/sources/azure/manual/batch-batch-accounts.go b/sources/azure/manual/batch-batch-accounts.go index 5dda3207..b0e2383c 100644 --- a/sources/azure/manual/batch-batch-accounts.go +++ b/sources/azure/manual/batch-batch-accounts.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/go/sdpcache" diff --git a/sources/azure/manual/batch-batch-accounts_test.go b/sources/azure/manual/batch-batch-accounts_test.go index 24143020..0d37f1a2 100644 --- a/sources/azure/manual/batch-batch-accounts_test.go +++ b/sources/azure/manual/batch-batch-accounts_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/go/discovery" diff --git a/sources/azure/manual/batch-batch-application.go b/sources/azure/manual/batch-batch-application.go index 6917520f..a551e962 100644 --- a/sources/azure/manual/batch-batch-application.go +++ b/sources/azure/manual/batch-batch-application.go @@ -4,7 +4,7 @@ import ( "context" "errors" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/go/sdpcache" diff --git a/sources/azure/manual/batch-batch-application_test.go b/sources/azure/manual/batch-batch-application_test.go index d5c20fd3..32f38ee2 100644 --- a/sources/azure/manual/batch-batch-application_test.go +++ b/sources/azure/manual/batch-batch-application_test.go @@ -5,7 +5,7 @@ import ( "errors" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/go/discovery" diff --git a/sources/azure/manual/batch-batch-pool.go b/sources/azure/manual/batch-batch-pool.go index 9575116a..fd473fa9 100644 --- a/sources/azure/manual/batch-batch-pool.go +++ b/sources/azure/manual/batch-batch-pool.go @@ -8,7 +8,7 @@ import ( "net/url" "strings" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" "github.com/overmindtech/cli/go/discovery" "github.com/overmindtech/cli/go/sdp-go" "github.com/overmindtech/cli/go/sdpcache" @@ -288,27 +288,7 @@ func (b batchBatchPoolWrapper) azurePoolToSDPItem(pool *armbatch.Pool, accountNa } } - // Link to certificates referenced by the pool (Properties.Certificates) - // ID format: .../batchAccounts/{account}/certificates/{thumbprint} - if pool.Properties != nil && pool.Properties.Certificates != nil { - for _, certRef := range pool.Properties.Certificates { - if certRef == nil || certRef.ID == nil || *certRef.ID == "" { - continue - } - params := azureshared.ExtractPathParamsFromResourceID(*certRef.ID, []string{"batchAccounts", "certificates"}) - if len(params) < 2 { - continue - } - sdpItem.LinkedItemQueries = append(sdpItem.LinkedItemQueries, &sdp.LinkedItemQuery{ - Query: &sdp.Query{ - Type: azureshared.BatchBatchCertificate.String(), - Method: sdp.QueryMethod_GET, - Query: shared.CompositeLookupKey(params[0], params[1]), - Scope: scope, - }, - }) - } - } + // Note: armbatch v4 removed Certificates from PoolProperties; certificate refs are no longer linked from pools. // Link to storage accounts and IP/DNS from MountConfiguration seenIPs := make(map[string]struct{}) @@ -632,7 +612,6 @@ func (b batchBatchPoolWrapper) PotentialLinks() map[shared.ItemType]bool { azureshared.NetworkPublicIPAddress: true, azureshared.ManagedIdentityUserAssignedIdentity: true, azureshared.BatchBatchApplicationPackage: true, - azureshared.BatchBatchCertificate: true, azureshared.StorageAccount: true, azureshared.StorageBlobContainer: true, azureshared.ComputeImage: true, diff --git a/sources/azure/manual/batch-batch-pool_test.go b/sources/azure/manual/batch-batch-pool_test.go index f1e1f421..93648924 100644 --- a/sources/azure/manual/batch-batch-pool_test.go +++ b/sources/azure/manual/batch-batch-pool_test.go @@ -5,7 +5,7 @@ import ( "errors" "testing" - "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" "go.uber.org/mock/gomock" "github.com/overmindtech/cli/go/discovery" @@ -242,9 +242,6 @@ func TestBatchBatchPool(t *testing.T) { if !links[azureshared.BatchBatchApplicationPackage] { t.Error("PotentialLinks() should include BatchBatchApplicationPackage") } - if !links[azureshared.BatchBatchCertificate] { - t.Error("PotentialLinks() should include BatchBatchCertificate") - } if !links[azureshared.NetworkPublicIPAddress] { t.Error("PotentialLinks() should include NetworkPublicIPAddress") } diff --git a/sources/azure/shared/mocks/mock_batch_accounts_client.go b/sources/azure/shared/mocks/mock_batch_accounts_client.go index 5f3f946c..f02310f1 100644 --- a/sources/azure/shared/mocks/mock_batch_accounts_client.go +++ b/sources/azure/shared/mocks/mock_batch_accounts_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armbatch "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + armbatch "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_batch_application_client.go b/sources/azure/shared/mocks/mock_batch_application_client.go index 9ef5f92a..70bea7df 100644 --- a/sources/azure/shared/mocks/mock_batch_application_client.go +++ b/sources/azure/shared/mocks/mock_batch_application_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armbatch "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + armbatch "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) diff --git a/sources/azure/shared/mocks/mock_batch_pool_client.go b/sources/azure/shared/mocks/mock_batch_pool_client.go index d973e2de..282603e0 100644 --- a/sources/azure/shared/mocks/mock_batch_pool_client.go +++ b/sources/azure/shared/mocks/mock_batch_pool_client.go @@ -13,7 +13,7 @@ import ( context "context" reflect "reflect" - armbatch "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v3" + armbatch "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/batch/armbatch/v4" clients "github.com/overmindtech/cli/sources/azure/clients" gomock "go.uber.org/mock/gomock" ) From 8dede06e0ec0feda95851275790eeb60f0ea5057 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:48:44 +0200 Subject: [PATCH 70/74] chore(deps): lock file maintenance (#4182) 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](../issues/370) 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). Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> GitOrigin-RevId: 9bb7cfcb7fdb5f30b717a8044cae9f25f935505b --- .terraform.lock.hcl | 58 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index d517b05a..fd321991 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.34.0" + version = "6.35.1" constraints = ">= 4.56.0" hashes = [ - "h1:6yGUU6VTNf/7lBfT+TMY5L8W2crrEGAd45y7/mNzkAM=", - "h1:7T2XHD73DzPlDSa0n7A6zZepkMRNn/N/U6E1DnqElgk=", - "h1:BEEUCSQqnbk/CyUAd/+4bcdtCpihkuoAfFDKCRMLUdc=", - "h1:HMD2NsuZiYGq09bi3vuBGY+evwKBDJHWIK4sWCEdnJU=", - "h1:Ku0EN0HA/3RU8T7M/NCMboOZHyRTi3ULdP/6hnENHSc=", - "h1:OWukIDuti3ZzrjDlrlvEGu8mHRM2VjS17XPWPxZxVHc=", - "h1:Qzr5C24XLiHmkJVuao/Kb+jFLPaxGE/D5GUgko5VdWg=", - "h1:ZGSMOPC+Du0cKJ2kV1Ni8Rnz7ezsIu+jYFEknpye5CM=", - "h1:aG6yVfT0GtYeKbB1rcqMkI/8MCykAkwjWnPzJtisGLA=", - "h1:gZ2YluA7pZ3OB1kIqhhKEsRFHxAF67+lqDm8aCtzr3g=", - "h1:rr+aNXi0UZ5Iwhg7dq0wGflsqTfms6h0cLFBArE+Y+k=", - "h1:tG2N7S54admlDbTjy5/T8QzGivR0WUcBOEahlZRnxUw=", - "h1:wXPejniDcbqRtL2zzaeZsmjLe7NekeYD5QjlIzUOylI=", - "h1:xqm4+M0cYrQRR2VMMU++W2Busqsrg5/VX1f6BnG35Cw=", - "zh:1e49dc96bf50633583e3cbe23bb357642e7e9afe135f54e061e26af6310e50d2", - "zh:45651bb4dad681f17782d99d9324de182a7bb9fbe9dd22f120fdb7fe42969cc9", - "zh:5880c306a427128124585b460c53bbcab9fb3767f26f796eae204f65f111a927", - "zh:71fa9170989b3a1a6913c369bd4a792f4a3e2aab4024c2aff0911e704020b058", - "zh:8d48628fb30f11b04215e06f4dd8a3b32f5f9ea2ed116d0c81c686bf678f9185", + "h1:0/uXxSpL98lpRqjRhjAvvWZVnJZnbOehfAlTrcPXURI=", + "h1:3AfkMHiID/TK41i9ipxdUbSx3v4hjIePNEZR0fEuQJ4=", + "h1:JDzMZ25sVEhH1IfEvIOHSobkAG6zVR9XJheIo/1Rxcg=", + "h1:LXMHE13aDvLoetIJuf5sSgg4Aop5iBZN+NKHfbG5zDc=", + "h1:OWVRgvaFuQ/uIysY2FJpLBA2syuDy2riakYgRxu8Vfs=", + "h1:RaWReVCoriJ00TKG4aLdybf291yTrhkTebXAB7gOOYg=", + "h1:Rkp2NXMY5RwM6J9bmep0l98i5mGBZ2yMhC+9nNaslUc=", + "h1:ZXdXwOk/VKPMcYlqEDevz+jyj4zoZKEV8D5zXOt3Lts=", + "h1:bau+2wU1EyOYFYbVSr+ldecm/JsRWrH/EJ2rPlSIT2Y=", + "h1:hNOrSOTUfwctGIrrd2033JXCcmA+zt+eeY2SNkg0Aq8=", + "h1:mj5knyv94JKLLBTwlTEv5Yn4MDAWYRPaYkCbZFhRYdU=", + "h1:qegJgE+n90ruKoC7xx+as0s9JSO64pRvCtw5Bxs6bTE=", + "h1:tjis0/YUzxXTgD7zaDS/ZVNyOU8oysKvso7GTteFaYU=", + "h1:xD+5zPhF0ry3sutriARfFVIg5m38VwYt66RveI3aUyI=", + "zh:0a16d1b0ba9379e5c5295e6b3caa42f0b8ba6b9f0a7cc9dbe58c232cf995db2d", + "zh:4b2e69907a1a2c557e45ef590f9fd6187ab5bf90378346ba9f723535e49ce908", + "zh:56bdafda0d629e15dc3dd9275b54f1fb953e2e09a3bc1a34e027da9d03ea4893", + "zh:5b84e933989150249036f84faad221dce0daa9d3043ff24401547e18f00b121e", + "zh:70bac98c27a14cb2cedabd741a1f7f1bab074c127efdcf02b54dbcf0d03db3cc", + "zh:7184f48bd077eaf68e184fd44f97e2d971cb77c59a68aedb95a0f8dc01b134fe", + "zh:7367589ae8b584bfcd83c973f5003e15010a453349c017a0d2cca8772d4fcfd9", + "zh:7ec9699dee49dd31bbc2d0e50fa1fff451eee5c1d9fd59bca7412acb49ce6594", + "zh:92dd139b96977a64af0e976cd06e84921033678ab97550f1b687c0ea54a8e82c", "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", - "zh:a6885766588fcad887bdac8c3665e048480eda028e492759a1ea29d22b98d509", - "zh:a6ce9f5e7edc2258733e978bba147600b42a979e18575ce2c7d7dcb6d0b9911f", - "zh:c88d8b7d344e745b191509c29ca773d696da8ca3443f62b20f97982d2d33ea00", - "zh:cae90d6641728ad0219b6a84746bf86dd1dda3e31560d6495a202213ef0258b6", - "zh:cc35927d9d41878049c4221beb1d580a3dbadaca7ba39fb267e001ef9c59ccb3", - "zh:d9e1cb00dc33998e1242fb844e4e3e6cf95e57c664dc1eb55bb7d24f8324bad3", - "zh:f3dbf4a1b7020722145312eb4425f3ea356276d741e3f60fb703fc59a1e2d9fd", - "zh:faba832cc9d99a83f42aaf5a27a4c7309401200169ef04643104cfc8f522d007", - "zh:fcd3f30b91dbcc7db67d5d39268741ffa46696a230a1f2aef32d245ace54bf65", + "zh:9f2df575a5b010db60068668c48806595a3d617a2c0305035283fe8b72f07b19", + "zh:a4602b7602c75c8f726bdc7e706dc5c26736e47cc8381be01386aa8d8d998403", + "zh:bc25fefeeee10425df7aebfc21dc6532d19acdf03fa97b9e6d8c113adffd0a1d", + "zh:f445592040b5fc368a12e6edeffc951b2eb41e86413c4074638a13376e25a9cc", + "zh:ff43962a48bd8f85e17188736bbd3c145b6a1320bd8303221f6b4f9ec861e1e6", ] } From 00b1b015c08da984aed03774fe5569fff45fc0d9 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Mon, 9 Mar 2026 11:52:19 +0100 Subject: [PATCH 71/74] fix: always attach goroutine profile on stuck waitgroup detection (#4185) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit singleflight.Group.Do returns shared=true for ALL callers when multiple hit concurrently — including the original. The previous !profileShared gate meant no caller ever stored the profile in the thundering-herd scenario, which is exactly when the diagnostic data is most needed. Removed the shared return value from captureGoroutineSummary since the singleflight still deduplicates the expensive pprof capture itself. --- > [!NOTE] > **Low Risk** > Low risk diagnostic change that only affects stuck waitgroup tracing/logging paths, with a small potential increase in trace payload size. > > **Overview** > When `ExecuteQuery` detects a cancelled context and the adapter waitgroup remains stuck, the `waitgroup.stuck` span event now **always** includes the compacted goroutine pprof summary. > > This simplifies `captureGoroutineSummary` to return only the shared profile string (still singleflight-deduped) and removes the prior conditional that could omit the profile under concurrent callers; it also makes minor formatting/cleanup changes (e.g., grouping atomic counters). > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit ddd2cddfb4d1c66003535bc296c4b11e9cb377e1. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: db976f32f2cae318e5f8f5637e850e3de21b9440 --- go/discovery/enginerequests.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/go/discovery/enginerequests.go b/go/discovery/enginerequests.go index 0cbce5ee..c7f8ac00 100644 --- a/go/discovery/enginerequests.go +++ b/go/discovery/enginerequests.go @@ -187,10 +187,9 @@ func compactGoroutineProfile(s string) string { // captureGoroutineSummary returns a compacted goroutine profile (pprof debug=1) // truncated to maxBytes, deduplicated via singleflight. When many ExecuteQuery // goroutines hit the stuck timeout simultaneously, only one runs the -// (stop-the-world) pprof capture; the rest share its result. The shared return -// value indicates whether this caller reused another's capture. -func captureGoroutineSummary(maxBytes int) (profile string, shared bool) { - v, _, shared := goroutineProfileGroup.Do("goroutine-profile", func() (any, error) { +// (stop-the-world) pprof capture; the rest share its result. +func captureGoroutineSummary(maxBytes int) string { + v, _, _ := goroutineProfileGroup.Do("goroutine-profile", func() (any, error) { var buf bytes.Buffer _ = pprof.Lookup("goroutine").WriteTo(&buf, 1) s := compactGoroutineProfile(buf.String()) @@ -199,11 +198,13 @@ func captureGoroutineSummary(maxBytes int) (profile string, shared bool) { } return s, nil }) - return v.(string), shared + return v.(string) } -var listExecutionPoolCount atomic.Int32 -var getExecutionPoolCount atomic.Int32 +var ( + listExecutionPoolCount atomic.Int32 + 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. @@ -342,17 +343,14 @@ func (e *Engine) ExecuteQuery(ctx context.Context, query *sdp.Query, responses c return case <-time.After(longRunningAdaptersTimeout): // If we're here, then the wait group didn't finish in time - goroutineSummary, profileShared := captureGoroutineSummary(48_000) + goroutineSummary := captureGoroutineSummary(48_000) expandedMutex.RLock() - stuckAttrs := []attribute.KeyValue{ + span.AddEvent("waitgroup.stuck", trace.WithAttributes( attribute.Int("ovm.stuck.goroutineCount", runtime.NumGoroutine()), attribute.Int("ovm.stuck.totalQueries", totalQueries), attribute.Int("ovm.stuck.remainingQueries", len(expanded)), - } - if !profileShared { - stuckAttrs = append(stuckAttrs, attribute.String("ovm.stuck.goroutineProfile", goroutineSummary)) - } - span.AddEvent("waitgroup.stuck", trace.WithAttributes(stuckAttrs...)) + attribute.String("ovm.stuck.goroutineProfile", goroutineSummary), + )) for q, adapter := range expanded { span.AddEvent("waitgroup.stuck.adapter", trace.WithAttributes( attribute.String("ovm.stuck.adapter", adapter.Name()), From f8bff30ca6d0422db19adeb1b6b0dd9b270c2fda Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Mon, 9 Mar 2026 12:43:49 +0100 Subject: [PATCH 72/74] feat: add MCP server skeleton with OAuth PRM and JWT auth (#4131) Integrate MCP Go SDK v1.4.0 with Streamable HTTP (stateless) behind admin:read JWT middleware. Serves Protected Resource Metadata (RFC 9728) at /.well-known/oauth-protected-resource for MCP client OAuth discovery. Handlers mount before the /area51/ catch-all to avoid route shadowing. Devcontainer nginx proxies .well-known endpoints to api-server and Auth0. Using local JWT: image Using the search accounts tool with mock data: image --- > [!NOTE] > **Medium Risk** > Adds new externally reachable endpoints (`/area51/mcp` plus an unauthenticated `/.well-known/oauth-protected-resource`) and adjusts routing/proxying for OAuth discovery, which could impact auth or request routing if misconfigured. > > **Overview** > **Adds an Area51 MCP server skeleton** using the MCP Go SDK via stateless Streamable HTTP, mounting `/area51/mcp` behind the existing JWT middleware and registering an initial read-only `search_accounts` tool with stubbed results (plus tests). > > **Enables OAuth discovery for MCP clients** by serving Protected Resource Metadata at `/.well-known/oauth-protected-resource` (including a configurable pre-registered MCP client ID), wiring new config/env for `API_SERVER_API_DNS` and `API_SERVER_MCP_CLIENT_ID`, and updating the devcontainer nginx proxy to forward `.well-known` discovery endpoints (including proxying Auth0 OIDC metadata) and Cursor MCP config to target the new endpoint. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit cd166b3d4bc2493445c36e38c3158d7d46b874cd. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: babc653a2dd493aeee7c3141818f98d7e5d47a1f --- go.mod | 4 ++++ go.sum | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/go.mod b/go.mod index b90fdf83..ed3a5238 100644 --- a/go.mod +++ b/go.mod @@ -132,6 +132,7 @@ require ( 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/modelcontextprotocol/go-sdk v1.4.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 @@ -324,6 +325,7 @@ require ( 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/jsonschema-go v0.4.2 // 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 @@ -436,6 +438,7 @@ 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/segmentio/encoding v0.5.3 // 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 @@ -471,6 +474,7 @@ require ( 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/yosida95/uritemplate/v3 v3.0.2 // 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 diff --git a/go.sum b/go.sum index 4da8ec2d..5cec1aad 100644 --- a/go.sum +++ b/go.sum @@ -601,6 +601,8 @@ github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u 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/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= +github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= 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= @@ -840,6 +842,8 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua 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/modelcontextprotocol/go-sdk v1.4.0 h1:u0kr8lbJc1oBcawK7Df+/ajNMpIDFE41OEPxdeTLOn8= +github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E= 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= @@ -1022,6 +1026,8 @@ github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TV 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/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w= +github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= 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= @@ -1168,6 +1174,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ 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/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= +github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= 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= From 987798e74062a16eebe7c9388267468cf4ae62b5 Mon Sep 17 00:00:00 2001 From: TP Honey Date: Mon, 9 Mar 2026 13:24:38 +0000 Subject: [PATCH 73/74] Fix paths in .goreleaser.yaml for tracing version (#4187) > [!NOTE] > **Low Risk** > Low risk config-only change; it just updates the `-X` linker flag path used during builds to set the tracing version and should only affect release build metadata. > > **Overview** > Fixes GoReleaser build configuration so the `-X` ldflag that injects the CLI version targets `github.com/overmindtech/cli/go/tracing.version` (instead of the old `github.com/overmindtech/cli/tracing.version`) for both Linux/Windows and macOS builds. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9bc6780a800b9b027088363b3ee96150a2083b5d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). GitOrigin-RevId: 62b1585503d1ff5191efcdd2d3b210f05315b4f8 --- .goreleaser.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 926cb565..2fc26655 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -9,7 +9,7 @@ builds: - linux - windows ldflags: - - -s -w -X github.com/overmindtech/cli/tracing.version={{.Version}} + - -s -w -X github.com/overmindtech/cli/go/tracing.version={{.Version}} - binary: overmind id: overmind-macos env: @@ -17,7 +17,7 @@ builds: goos: - darwin ldflags: - - -s -w -X github.com/overmindtech/cli/tracing.version={{.Version}} + - -s -w -X github.com/overmindtech/cli/go/tracing.version={{.Version}} # For now we are going to disable signing MacOS packages. This works on Dylan's # person laptop, but we haven't worked out a way to get this set up in a github From 02c7feb299f4b32142b375026009d693a79b9363 Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Mon, 9 Mar 2026 14:01:33 +0000 Subject: [PATCH 74/74] Run go mod tidy --- go.mod | 198 +--------------- go.sum | 735 --------------------------------------------------------- 2 files changed, 6 insertions(+), 927 deletions(-) diff --git a/go.mod b/go.mod index ed3a5238..51b9b4a6 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( cloud.google.com/go/bigtable v1.42.0 cloud.google.com/go/certificatemanager v1.9.6 cloud.google.com/go/compute v1.56.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.16.0 @@ -40,8 +40,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 @@ -60,12 +58,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.1001 - 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.6.0 github.com/auth0/go-jwt-middleware/v2 v2.3.1 github.com/aws/aws-sdk-go-v2 v1.41.3 github.com/aws/aws-sdk-go-v2/config v1.32.11 @@ -91,84 +83,49 @@ require ( github.com/aws/aws-sdk-go-v2/service/rds v1.116.2 github.com/aws/aws-sdk-go-v2/service/route53 v1.62.3 github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 - github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.4 github.com/aws/aws-sdk-go-v2/service/sns v1.39.13 github.com/aws/aws-sdk-go-v2/service/sqs v1.42.23 github.com/aws/aws-sdk-go-v2/service/ssm v1.68.2 github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 github.com/aws/smithy-go v1.24.2 - github.com/bombsimon/logrusr/v4 v4.1.0 - github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 - github.com/brianvoe/gofakeit/v7 v7.14.1 github.com/cenkalti/backoff/v5 v5.0.3 github.com/charmbracelet/glamour v0.10.0 github.com/coder/websocket v1.8.14 - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/getsentry/sentry-go v0.43.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.13 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-20260224005459-813a97530220 github.com/hashicorp/terraform-plugin-framework v1.18.0 github.com/hashicorp/terraform-plugin-go v0.30.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.17 - 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.4 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/modelcontextprotocol/go-sdk v1.4.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.49.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.26.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/otelpgx v0.10.1-0.20260303210427-65bf1016045e 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.31.0 - github.com/riverqueue/river/riverdriver/riverpgxv5 v0.31.0 - github.com/riverqueue/river/rivertype v0.31.0 - 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.4.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.18.0 go.etcd.io/bbolt v1.4.3 @@ -198,15 +155,10 @@ require ( k8s.io/api v0.35.2 k8s.io/apimachinery v0.35.2 k8s.io/client-go v0.35.2 - k8s.io/component-base v0.35.2 - modernc.org/sqlite v1.46.1 - riverqueue.com/riverui v0.15.0 - sigs.k8s.io/controller-runtime v0.23.3 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 @@ -221,23 +173,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.6 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect @@ -253,14 +197,6 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // 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.4.2 // 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 @@ -271,69 +207,39 @@ require ( github.com/charmbracelet/x/term v0.2.2 // indirect github.com/charmbracelet/x/termios v0.1.1 // indirect github.com/charmbracelet/x/windows v0.2.2 // indirect - github.com/cheggaaa/pb/v3 v3.1.4 // indirect - github.com/chzyer/readline v1.5.1 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cloudflare/circl v1.6.3 // 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.18.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-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // 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.1 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/golang/snappy v0.0.4 // indirect github.com/google/cel-go v0.27.0 // 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/jsonschema-go v0.4.2 // 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.12 // 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.28.0 // 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 @@ -343,7 +249,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 @@ -354,38 +259,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.3.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.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.19 // 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 @@ -397,106 +284,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.8.2 // 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.31.0 // indirect - github.com/riverqueue/river/rivershared v0.31.0 // 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/segmentio/encoding v0.5.3 // 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/yosida95/uritemplate/v3 v3.0.2 // 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.41.0 // indirect go.opentelemetry.io/otel/schema v0.0.12 // indirect go.opentelemetry.io/otel/sdk/metric v1.41.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.33.0 // indirect @@ -506,24 +328,16 @@ 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-20260209200024-4cfbd4190f57 // 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 k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // 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 5cec1aad..ebb34705 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= @@ -18,15 +16,6 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= charm.land/lipgloss/v2 v2.0.0 h1:sd8N/B3x892oiOjFfBQdXBQp3cAkvjGaU5TvVZC3ivo= charm.land/lipgloss/v2 v2.0.0/go.mod h1:w6SnmsBFBmEFBodiEDurGS/sdUY/u1+v72DqUzc6J14= -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.119.0 h1:Fum1ighlxsmwbmaf0nhuMDebcKJkpx2mgmd1YcyXaYY= @@ -35,8 +24,6 @@ cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= 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.74.0 h1:Q6bAMv+eyvufOpIrfrYxhM46qq1D3ZQTdgUDQqKS+n8= cloud.google.com/go/bigquery v1.74.0/go.mod h1:iViO7Cx3A/cRKcHNRsHB3yqGAMInFBswrE9Pxazsc90= cloud.google.com/go/bigtable v1.42.0 h1:SREvT4jLhJQZXUjsLmFs/1SMQJ+rKEj1cJuPE9liQs8= @@ -45,7 +32,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.56.0 h1:e8xch/mR0tJoUBj3nhNb96+MOQ1JGVGB+rBfVzWEU5I= cloud.google.com/go/compute v1.56.0/go.mod h1:fMFC0mRv+fW2ISg7M3tpDfpZ+kkrHpC/ImNFRCYiNK0= -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= @@ -56,7 +42,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.16.0 h1:0g2hnjlQ8SQTnNeu+Bqqa61QPssfSZF3t+9ldRmx+VQ= cloud.google.com/go/dataproc/v2 v2.16.0/go.mod h1:HlzFg8k1SK+bJN3Zsy2z5g6OZS1D4DYiDUgJtF0gJnE= -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= @@ -75,8 +60,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= @@ -89,8 +72,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= @@ -99,13 +80,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= @@ -154,10 +130,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= @@ -181,29 +155,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.1001 h1:yHDTgexACdJttyiyamcTHXr2QkIeVF1MukLy44EAhMY= -github.com/a-h/templ v0.3.1001/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= @@ -214,12 +169,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= @@ -229,11 +178,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.6.0 h1:KCoLxTcH8qXPYbwKZxxFrL/6P+P+Zc58BQPL6w0Kt30= -github.com/auth0/go-auth0/v2 v2.6.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.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA= @@ -304,8 +249,6 @@ github.com/aws/aws-sdk-go-v2/service/route53 v1.62.3 h1:JRPXnIr0WwFsSHBmuCvT/uh0 github.com/aws/aws-sdk-go-v2/service/route53 v1.62.3/go.mod h1:DHddp7OO4bY467WVCqWBzk5+aEWn7vqYkap7UigJzGk= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA= -github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.4 h1:PEz6RPI6hG3GHiaMPmrh4iM684GOdIrEc9L30FaSC2k= -github.com/aws/aws-sdk-go-v2/service/sesv2 v1.59.4/go.mod h1:4dOflh7HfqHcjF1OIlt9Tr1T0rDsh906Yc75lAa2CJI= github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row= github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU= github.com/aws/aws-sdk-go-v2/service/sns v1.39.13 h1:8xP94tDzFpgwIOsusGiEFHPaqrpckDojoErk/ZFZTio= @@ -322,50 +265,18 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nni github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= -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.4.0 h1:TKnLPh7IbnizJIBKFWa9mKayRUBQ9Kh1BPCk6w2PnYM= github.com/aymanbagabas/go-udiff v0.4.0/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w= 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.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE= -github.com/bits-and-blooms/bitset v1.24.4/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.1 h1:a7fe3fonbj0cW3wgl5VwIKfZtiH9C3cLnwcIXWT7sow= -github.com/brianvoe/gofakeit/v7 v7.14.1/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.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY= @@ -390,36 +301,19 @@ github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8 github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= -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/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= 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= @@ -427,42 +321,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/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.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= @@ -470,23 +344,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.43.0 h1:XbXLpFicpo8HmBDaInk7dum18G9KSLcjZiyUKS+hLW4= github.com/getsentry/sentry-go v0.43.0/go.mod h1:XDotiNZbgf5U8bPDUAfvcFmOnMQQceESxyKaObSssW0= -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= @@ -495,77 +358,36 @@ 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-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= -github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 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.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/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= @@ -574,45 +396,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/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= -github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= -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= @@ -621,8 +415,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.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ= github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -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= @@ -633,14 +425,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.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= -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.13 h1:lLiliIivyXW/8L7n244q45hdVKNCYprnfztyes4ew7k= -github.com/harness/harness-go-sdk v0.7.13/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= @@ -664,10 +450,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= @@ -700,24 +482,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= @@ -726,39 +492,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.17 h1:fkEom1MBG98QeN7uaJpKBRA9st3bPdS32RK+im/IjCU= -github.com/kaptinlin/jsonrepair v0.2.17/go.mod h1:Hbq/F0frQBVClHW/oQXixaCPysZ6gdzpeUBZPpWQtAQ= 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= @@ -768,36 +516,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.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.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.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= @@ -810,14 +534,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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -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.4 h1:1aMhJ+0CkvUJ8HGN1chX+noXHs8uvGLkD7xIBeYd31c= github.com/micahhausler/aws-iam-policy v0.4.4/go.mod h1:H+yWljTu4XWJjNJJYgrPUai0AUTGNHc8pumkN57/foI= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= @@ -826,14 +542,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= @@ -842,16 +554,12 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua 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/modelcontextprotocol/go-sdk v1.4.0 h1:u0kr8lbJc1oBcawK7Df+/ajNMpIDFE41OEPxdeTLOn8= -github.com/modelcontextprotocol/go-sdk v1.4.0/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E= 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= 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= @@ -870,36 +578,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.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE= -github.com/openai/openai-go/v3 v3.26.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/otelpgx v0.10.1-0.20260303210427-65bf1016045e h1:vP/Zs8Nbd902stVf7hBOd3VP/lIECgAjWR8pNBwcOu4= -github.com/overmindtech/otelpgx v0.10.1-0.20260303210427-65bf1016045e/go.mod h1:GtSjAg9Irz03mc8tPIh9/bKOx63sqyO752SABZhBdj0= 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= @@ -914,58 +602,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= @@ -975,80 +618,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.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= -github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= -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.31.0 h1:BERwce/WS4Guter0/A3GyTDP+1rxl6vFHyBQv+U/5tM= -github.com/riverqueue/river v0.31.0/go.mod h1:Aqbb/jBrFMvh6rbe6SDC6XVZnS0v1W+QQPjejRvyHzk= -github.com/riverqueue/river/riverdriver v0.31.0 h1:XwDa8DqkRxkqMqfdLOYTgSykiTHNSRcWG1LcCg/g0ys= -github.com/riverqueue/river/riverdriver v0.31.0/go.mod h1:Vl6XPbWtjqP+rqEa/HxcEeXeZL/KPCwqjRlqj+wWsq8= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.31.0 h1:Zii6/VNqasBuPvFIA98xgjz3MRy2EvMm6lMyh1RtWBw= -github.com/riverqueue/river/riverdriver/riverpgxv5 v0.31.0/go.mod h1:z859lpsOraO3IYWjY9w8RZec5I0BAcas9rjZkwxAijU= -github.com/riverqueue/river/rivershared v0.31.0 h1:KVEp+13jnK9YOlMUKnR0eUyJaK+P/APcheoSGMfZArA= -github.com/riverqueue/river/rivershared v0.31.0/go.mod h1:Wvf489bvAiZsJm7mln8YAPZbK7pVfuK7bYfsBt5Nzbw= -github.com/riverqueue/river/rivertype v0.31.0 h1:O6vaJ72SffgF1nxzCrDKd4M+eMZFRlJpycnOcUIGLD8= -github.com/riverqueue/river/rivertype v0.31.0/go.mod h1:D1Ad+EaZiaXbQbJcJcfeicXJMBKno0n6UcfKI5Q7DIQ= -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/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w= -github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= 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= @@ -1067,12 +654,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= @@ -1085,61 +670,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.4.0 h1:JMQMqb+mhW6tns+eYA3G5SZiaoD2ULwN0lZ+kNjWAsY= -github.com/stripe/stripe-go/v84 v84.4.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= @@ -1151,12 +685,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= @@ -1170,23 +698,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/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= -github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= 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.18.0 h1:pJ8+HNI4gFoyRNqVE37wWbJWVw43BZczFo7KUoRczaA= github.com/zclconf/go-cty v1.18.0/go.mod h1:qpnV6EDNgC1sns/AleL1fvatHw72j+S+nS+MJ+T2CSg= github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= @@ -1195,24 +712,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= @@ -1227,8 +728,6 @@ go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls= -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.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8= @@ -1255,278 +754,101 @@ 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.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -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.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= -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.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.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg= google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE= 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-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -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.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/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= @@ -1534,91 +856,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.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw= k8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60= -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.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= k8s.io/apimachinery v0.35.2/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.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o= k8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g= -k8s.io/component-base v0.35.2 h1:btgR+qNrpWuRSuvWSnQYsZy88yf5gVwemvz0yw79pGc= -k8s.io/component-base v0.35.2/go.mod h1:B1iBJjooe6xIJYUucAxb26RwhAjzx0gHnqO9htWIX+0= 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.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= -modernc.org/sqlite v1.46.1/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.15.0 h1:7Xm/tqv63jZrGSv4X2u4zpAvbtXSs835Qk4RFonBDdk= -riverqueue.com/riverui v0.15.0/go.mod h1:J4fH8+zPe1cqmYWuMWVJdDdMmq1U2UPVofyOczGZNnw= -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.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= -sigs.k8s.io/controller-runtime v0.23.3/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=

#o^08*x-gg$QT%+ zof7tm@?8Bj)>d3)N3~~Iy0Edajm2Mp{Nt`X=Qu@8f_>;bj=r7Hx-f%(WnJF7`tkUj z2@AsMDHKju&fW`zFVD9Rl^$93+w&kMjhgIR)63WhNw5)INhMxJt8aw;>tG%v(VurP zHt2=3@Fsdqew?d5dcy_e80Swj*Z&+g6O3e|$Yr`t>DO~{19_M}<=qFmiMU}7e~l^D zF8*foYs1I;t3aBMeA^g8lc?7*)d|1(vI)FZbJcqd#^Rm{hhh1c6^Z?3)EV&c>@0^C z{l>Y9J_4_N?$5E`K->4|hcFkb6EAh>U{5~2HrtkrpOw+Tqb?Vmr`x`(=&!KVRQq0+ zimd*F%*tqx!TQ)Wq1IHtNQKGJO9{b|K%QtguwG% z3#ndi^sT&vY~>22q0m*ga*ZdvjvsCPw-x%#$ss02kMg?uH}XGTB)e_x?gGZ!i|D}$ zMm-pG{mnp3VV-jm_jz$LhP@4ZXZ1C=^nUYgeJ0+`C>{w5v#-qT>;-9ib5eB@g3B}> z?$ziSH)4!uiw}&p!Sl%P?C9FHJXoMWdbzl`IuhQAyI;<|NKx{Aif-nkLQ_QKEIx-J zJv{emH)BNq9!ZTgJYQf!fnhU6Xz*ofM!om>Rlofm*^+$~YfI_U6|^Rx{e$|RQ<;pj z@b8OFdp*pR&DEMijNhzz1iOm(d|qJHM{lXw0RwRy-WdCH*vv4J?dXp`Wz+v2_J+ss zuouM-Bj^zPh7$Oz!#8s7H|B@{A8X%mV*GHn5xm45`H<7U`Eo8SI5FsPG-Lkn3Bj?X!!pMzI^eWrtP+3x+$4&WCxn zRnz(=z}P*1ioi(LY)kshw_m`+R9j<8Hz5`T-2vyly0^Mi_&4?hKEA%&k?~IVUc*Zm z_sA*mt^VcbC*iFQrS@zGSjoc=Iq&mQ+CiVqU;dE~68%AsgrVyU)4ab7rev60$n?t>=_9*h3ePTB z3{Rp9J!_<3TneL?EPkV~*@W9``P13Eat8j#kYjgF|8A#((Ba_rQ_gf9EaaylJM~lT z@Alp-vQxDzzx~Nr^GA@$%P`6-9JqeC(MW2iz`~IFHDUseC#+O7|TJQT-;Td>NPhsR{!z_ zWFfwmMDox*vJDU6uSlPVp1;|9C*T#g>wNra$+J(usMU^Ld;1#);V!aM@8UNzB$y&d zZJ-v8qTgV+pP#@_JJ0ZF(GzR#4Z+#G_sRff>S5|+= zGnz0?IoWi^IIK!Ge0C+w&$L_m4gxA6gCXfp<>xphW@a{kXh632{5Z5VBl^sY^m^pA$m6lE^dY;pba*wClDGd~;DtIfIgdXrh{(9zZvBt*X5Ma9oV zu7LPu5Ly>V>k-v&&GUh5UmDW*QPK`AH+?i4hM?JxG^7CE@)4I`b)rNXKFj|!yPU`Y z9X%Kn#U`DT$Fi;jI%DBKA8;hcu3mhGRSS=L zHwIOSwzhA@p1PE|+C8a#4R*jrv;ZS$5s8MqRxq<3XKP#W7{Pp?;@0WiO4H9DYD)5n z=KC+s63@N9jRZ=AAm2TL5D)WEExObdlROW+LG+VDrap@sP>O5@tJ%7L`e$?(#7Xfu zA*#sbGvLx#keUfj;Sip9e$?1k8$d=3}b@}57Q_2g=T7xtb z8#B>g- zE`~T_Y`&FZyP^9K5nzJ$QdQDRh0)ZU(s8U9-?Mz*xrsOamYBrk@)z3i*Ax}MRtw-8 zxjI(^#rk2HW0-$}2v^WASx&H(RyA?;CqI*DTe=HsnS$eXphhV*4CBU%3J4s%WOc%w z;Mo{xXj0(pgvhI0p=%5C=FVjhGN(``gU@2tj0mf=2HsaOWJ!Xlo|}}Zd?{`~NM3`f z=g)8ly{G~dp(9#X2lv{uIfly(?sQp%qS!L+mVspj23B7ej^lbD$GeAkYX?@;4-qZgn!6(F4dPo3(1lWn z=AvJxS@D26-ucmU^C7Jy{^d;BT(1-+#bAb$bzKP-H&}T2Q*A@MYT3n)Y5Nby$T;Ec zTVC8(bOLpAepidXu$!4u&3?5(b?-aDKb}NI2+| zeUeDm^mk>MG=!j=&!1aO9x8Ghjwu(H2P5FTm&PRil1(jW@M~iN8MJ2GD;2F?u5pF4 z$`|B8TCdM?A@d|VigmM)z~yw(K4`c0r~LHrZEmqzfZ=T3fQEr`_AwrMj5)vhma~Fx zme0bToNJV_uW6C(^{VDnE3vL#KlupikG?|kzPmZsdsL$Jj>n}KxbY-^)TJS;7pJm3 zRCz-HxZ>(TXu6s?@SOb+6#ex`7_c;d__$ZDLsMbu)y>)K z8hKV$JeyG0sk#;b&O=}5^r^nO7gah`?G>;Sy#eIOB9Q-gI+6bxZp%}}M1to}t!sTh z(wY)Fxag))Ifm#5WYo+?qJ;dXbtV=3X&xQctS<7Mj|rP`w%HHNRyk{*L(oJfa#fWI zyNUL6giCkZZ5c}4g(uE6*>IbEKCLzNgt2eAHr_bU@KnJ&TMjwV0hr7eSC5#KKvnYH zgZ`Tguq0g_?mOuR`Zv{G91cVJui7q54|`Cd+nzL=oH_e?JXrl(FQ77^eX^Vww?{E? z@oRuX<;yx@?E0a^mP^HOlO5p}GjJ>T{-m;B-(rPSE2QWX2`YyUs`RweC7~ur>uX>I8y_wRqySb`y5 z8u#s`r>TgrvQ%+lMN3N|H>%7++bijkgo*Fsc!36OWw}hX65T93h9t zb{e0D2N=@c1F+kwDGo2%v4@PfN&VWpl1vU~Qd#kpo{e^~pmuLms+t&WO1vXG?RYrX z4;l*8LM!9ai|#ID%y1WU?{t9r7_<%zmQs)%2uBn>)t6aD!6y3g^YkrgbNe zVbBiPP9qIW6v@_IgNiG*Ooy}q-mOfQ{xhwEq?GjRr1fIQXQJ2))SBJO1A7~V1Ago} zSJ&9o^C8f=A%sn%f^${T>~Wm>dOV-;lU;V#6<_{@suH(s(1TXp0tj!_XQ5gHy+enN z?8$fRJjW;P@L6z<6>nPLEX~0$GadHW3V@|l&=@hqQNyV(@FvctGP_KH$E3@a1(yc#I!s_eD`OAn)&AJgTSj zihVPT-7Y~?y?qHsoRx;VTQK_r(k1SkeD538&5EB{o>ii$(8Tv!7|Z0=O}f4_BQ|fs zg^<14e1&_6qnE$nF02*sr$NGK$Ytk?9WU>dTO3&52corHnM}uTM3RbC^ z-^WmOe#G@`H`IjZaD^-f?+BQoT0}+P>rwjn%ZG?)=v&yKH~oQ7)dxOL*En+i75)la z4G+AqyTR?fKK9tgp9lTV16zn_DiqO?4p;bobKh1MuXZFyX8&QgLp!k(C6ncYP#GgT zgxj_pG8`sabPrv}5eLJ2z_>o^{p0ASbf&H&=ihX=UdP2KH?(}q;ycwQ&GPiEkZ)@D z!F+|07YVf>^&DTpQjMIua^E4@t|gl3(77U(M`+|Fa$i7ez{!}0U(bH)#aC0I<Hr%Uu}ub|qDxJO za~ttt2Si-qrPdEc-8CE^p4jSy91qN&4qlyz;JPF&oB@(%ILcUg>`Zu~HhHJz?mgX3Zb z>wNooWdxHdlG9{+T|yR>tfGQhCvKGaGUi-FCE68IqijKmA;Z>eruUVI&O8;$=MfRC zg|YI6|5~hfH!#v@{O%5{!^%7G&pN+$POF6m2V$hbhD((#e(iw9or6x`Cs`4a<&H?p z5-%cNHI!_PXuMR!SBG@;$>fgk5o+i9Q5Plk7IDbOzdtd}cSJnzn_hvQ+O9W{j>67T z$u^lP2C~B~$q&=cP@qrf-B^u2a#^=KLLQ&7mho=8MzJG7GWMVmQrBbvH1@xiL@ zhfD~=VFz;c*n3qMp$)oXbGg57TGx(EW7i;yFU?c~^s{6z`jg^QPw+{)ttpknE&5dZ zHx6vZm`Yy91)B_$Jcmk3V%G{3O(8>lB|i7UZZFvfosgU=cqA^VZu7zx>>It$s|ozk zBpi|BfIlWm)NF%HK{(J5)qmS|E^ud)+vC7#XjFJGg(rzhhY(XmkR`mqKYmm4 zSdqAe3XPLdd||D9*KkD9?b0C^N{K~ih_gCw>RrQ`A8u%Q2gw^*)t}2jalWY5~&foWwB6o4b1*C0!&OrO41A4oQb!na! zyFP6_Enq#=?e_Ri7L+_pC8-5H-o97;W?_U3*M(V-g4C@Y7G`RV*|`a2m~LhtqSN~H zXeoubcobsDw>s0! zOKuKWsV%-LehXPsE^hUJZtNs~F@oft!UBj&irL|Iu?<+7I<1#(CYNAstyUaSB7Q$H zn1JaUS*f6{lq8!Lq}zApCMQEwvOB_eNFdupUV~HM+5Z9EY+$v6CDQqT$(*N2m)#t2yhD9QEf{Rw9$Y3X z{=mSa%GXms%N6>YxWpHUF3wF(`oHoMH4XZI_&W7pSnNJ6la(0zfy znf;4AKx?x9$&VGi?uLmpDOPW?W}X`^B!-AWHJcMZ9h09}>RH#5;w*SbfsqD*kHoAG z2&1I;NBfL+=1Y2izu?+tZ08-`TI_xiz#<7(P|`<3Xy>zNyefPs($1pG zfpR)ytRK&tHXM18RHrW2;3nIW4LHPX?sNzA75|8=EJqy8!`r&LcKmR z>5jdnovA>1>8G1@>ZceS;upSe$H4Udd3&NoY;f6|P)=6E`~huW!hO!L;2l`ldBmsB zX~LR(=*qj;%?~N9>jUWC_vc&ibFpryj8&(94O1-Im<2EXaJoKbc-DS+Ma(GE>Ub10 z-q~V5lSYOF<5g=Aa#-Q(1&ghJ2O$5n{kxgLM`@`Fu#K(syl*bRM4I--M#%2ge+Wly$La!VzifAU3p<-H3`|qk}0pKOkjl zLo5abSMzeWpkGSdn?N@QMVX9LTmzx&UP0`FOB><%xCa z$}L@E8bgxgu*ILGg7luo@n@dxzFzdBOrtnRONfJe!9yUbP%47_V#g02$HPUK~-+n!dO0ZH7_IH+(4l zW0k-Uj{OgoCSGi@K+_?>yBydM+~6FJC|{`<+07}w{1)61`~Roqy@l3`OLEkuahJmV zXh9ej`qSVN?`UD23#B+uQJpaSh`}_|kDJo8IaojcpOj*3YxwiI9BJ9LsCA5N=QT z5^mCdDaU~Y+AT-*>S1SRFVS&+u}fXei^*k0~Gzxv~@ zS`t#3G$_qq!Y5Y5Bi!DOO(B5Pdc1g~8|(G%tl1K(2js0S$}!`g;24@E@7?ggeS+kR zzj@$3?I$>*XY+nBHYq2dmLWh2 z#3LB7M<+BZaRPiIBh%BeuB;D<8p32#RABAESxO7zxe2TmLyZ*etm<9&c4Zx5k zfT0EB0lx+3b$k>lQYVGS}jY)CD9O=hz}KQueYNpuum--a;Cm9fXWZQs>`n z$^l_4LS{6V`FgrSsqc=(S0!FBtWz`%NHFvRRC5rTh{yl}69;8p!;=j&ENaze7MCDw zag3XT)K3|o;B}i78UiN3adH84fQxFv`3qFChAA0pOS$nTSW0y%MXPDXDs+Onng>e} zHY;{2aFccW5GE);a&K2d?xrOMzquX3Py&~xLgoVBLcLPEFmOCmM^6T6gGyBz`mQ{; z2e7qG(Xv<=3vK$O0Rwql1n)|QaHwhaGPF>M;e|dNQ2OZ3JftYbd>=xZ+zx`O9|aLa zVbo!3m8vI&;&)M=dSm)$(Ilskr7m2+!#vn9Iz}eEr@yl9&xg`n4rQ( zfsTtS%t$oA1xH7Y1l`{k>vgW>eAhfsJ5ffsuYjqZKNT({IMapDPCOP~mB9&>+adke zYW-#*M+@&SkNbC(r1cHCr+QNdb;5JD|6<&>oUtSDw&Zz#fneU`V8h)V7mE9JWgDlg znk$|d{?V}MeI{MmxCNmNYiH9%zw2+D`&PZVd(DBIxbkJ?Lvc)GEv5Na(_@{@&qjso zI0TM1jAefiw}p8aoD8IuaCo?D#}7f>pB)9ROLJHDJ;Bg5J>W(!c1$9HGda_`+L4=% z`Xcc%Q9bIMvTnWKNODqfc!3&5f5apY(v7=ET7ebm^wirxnk?>u-0TYwsMxeq!r@$eR%Q=_~!mwJcJ>0`(0ZJyVX*7fK#{a?CayII84^issmvKHG$EuQP(kJZ^hLHSHcWrzZTrcN)O22pYX4~8;0!-C0@iX8(E+Jm60qv;gsiFql zH+E>O+j`_idT>|ky*!6~F>Vj?F16byJ0STeT_c(thNHqthLfatyVlzQ?FFh0U7^o} zq2t`c%X%k!ASNHYD^+(XWK4JM3noozjJ?BG(^d2jTe!f#mhy6O5-Vv5v>+IxJTQcan8g*jdajg^ZuZ7bochbokbh{3Pen|_Y!IhDVpV&7g@E1T)i{JiBjBZkU$1J zqmGuuCKYsOVvfy|(vqx4&Zt;%fG(s0C2FddiXCLr+Ciil!{?s7$>A2f=&7+kQPvT-#HR4kjbK*%g z2wgHUN7H5@iXLfwa?to=Q1ODr*OKSNT{vmfrA+%U9dc;0oErB+F_9`!v2;Z*%|v=} zC{H)>?8Lo-mKw2zIwa5B49;n9z-j@>zI%>@D>L`sn_)M^%tK$;xKulf76=$wuM`}K13+Obs{g`jbJ5j3Ua|RhwUO2#yF)!{$Eo$N3n9y?pc~4WGmtrFv5p?dbtyZa z=&kA57#Fx=kKn@yXIWz2`~99OCm?-yZ!OgFl~2PxlYjh?=kdndDu+2NNg|?M0{G@6 zEm?%5<)}eb5ZTE~bNk6Ysd6Yaw!P=&BHYZsbxxNI{+RkSKKY!Kepe>q6Igev3Rgbo zwxAc4i~G@IZE4YzG&u$73r>^0W#b;;akFihsaVJMk@78TW$4>x&l0bxa}dY~`Wo#j z6P0M?vMK|CYFqFw)Sz&6s^ZjY!_Pf$uG2>p_q|Jm`Z^Q zfLB}V)3)dj+>dtEX%_^3CqG*slOg`RC)4`64pz?UowU1|1nP8^g zIFL*X#O$%=v>DQ)R<|t+={->Gn+SD=SE?_!OV_5R+-MWPFP(o~XeRJ);jV}a64bE_ zNuDm4c-wL$@Q;S+iQ@Km&hR@LI?SA~`N$ zzo!!|6*VLv>-<#q)eu0(*oE2gi-B^@Qlf>rGs#_1cCq#-N76-7m;R5$9i?@a7yhRM8s+68?T&sC&eyrLs$;5L8uB_t~uxF;~ll81)|nce&|ZllPw z7sRu295%_bP_5a^m3rVzvPm9^G8q7H7%TDo+b|;NQJ|tmzDr-PfxoS2gZiMH`5Nv` znLT%)FK#OX}@Eu zvgix5qARZgWPUx7Yxijz;D{x0rp52o3#Mlg;Yjphy|sxIECtJNuZ{Axoei~4z9z{K z`Q0#vpul4?-8kn>DI}U$)D;mSsk29EVaev!6d1M(5<#7hV2z%BB1* zqOr(z?9A>ETLwSJ!^lEV`!z9G;PGSj>&ulHv~x7Sd$3mmEsRs`pmxW+mb72SO3$#T zCd)A2d+84GqR-n*&G(xIeG-rSpif91xX%5jLx+tt%L8X|t+`Y_$)c}wmllVtpvJbd zXV#aKJmwdW5?!Xoe1}SBEWd$&5@k{5(`jDmja?p^Z|D9&I@{H!-zn$VaoEMkj&`JN zm3Vz4fW~)UG#up1{-Ac``dZskCz{4|KQmsJK&N>=Gj0UKJx>SATkbt(-*GV0)!HGh z0)R$Z_G9<1q@n`|+wWU0f0{csMBvIz!IAGk1x}0dP%a;m5`-oma&~0A4YW!?X}ui1H0i&ePElfL17{>~0CVkwv=YUI+Hy>10~8Y#U9;*_?V+69Kv1+l-O> z*|p@(Aq+>_Wb8^$alc>(c!+SHjq}traeyIS7 zTjBz=GsozpZKD8w#h)dke1XJ+J$7aJ=yE_~gT%LVGviZxNvS}pM{5Licns)V$ zOkASMK8`@rZB^+&Lh^WDAdP}thJ#SABd5B?A@1UWzY3LoeM*-kP_y1=sZXJjefO_wVX;kyRtjLR<=bD- zo~twCq1xdc*$bJ$&dc3>D}#Z|F6P$p3t7*VJXLpE0<)Lz&{TqR`2r|@L2EtU(ttaK z4b2Z6sr(;Pt>B17gKOQr{*6yQsENE?`}PL#)cxBm18RW)D?s93$wD{pDiLWGzEn(* zD(nH5V9W-h_3*!=b;RsA9F|QbRc#KEMIthLD0zoKxyx*bC`lYBzr_GJEUyjRNyS-T z=e9}YHZ7wjtw4}KNw!aESb_vl^$!M}r4a`O!KwMbR zAwa0~9iiyI9jPOqyCEFZk!*!V1n@^}L6`s3TcAe3GZHBkIoRSd-+dN_wi0+rHI zM85&&pH(8Qka}^C`B2@Re0>{|$~lUu#@ZiH+P)o|bgGt2nx|Z^Zq$sDd|fgDDa(17 zm&FAt)!A6SZBSAwJ*(gtbY7^1#4-VD{>nE~+GcdF!&#d4tC7tZY1VG=WTCL0!1?lp z5i$O-5kb#Rn}~k<#I6947dVNe-qBZlIGAn!QC7p{}S zM^D&M+wXUSEMwfB8Gj3VnIQu;Vum|#dh;Z-(b=*PE&8UX+z7Bbzcij@=W|Fuf|5mq z5m{cCp=w^{zMY=kD6d`h9%^`;^O+s9uWxF2T1ogoi-O{%{ilCqT&_H23ZMH1G0dl7YC2zK&`|yTYZ~oB525b-6-dWUZFm7p?@WR7j(JqwGR= zyPOLL)+Y?$q-AX~i;86PDxl`vviLEOBFdvuCcF`1#je>WcQ96>E6ZrnEi~9&BdGx(ppG-X176C<>QE-w9h7L4b#wB~~S8tJ5k2%&V z1R{iY3Q43dTujNgr=aM}K^e0olBRUeS0t2^&$SEXgXWtfe&o%`eaT#V4N{F7C)zh- zASiKHO+Uqs0&L+l;-%^pU9p)gHWFid8>aC6jIWU@Q%PVXQ82qHzbYGYJ{vdjp@4HH zX)c1(QgD6kEF`anl7zU=r93_f4CNn)ddauq{vW6D>=LC7>2DX}C;LGZvtu-K{Jh~X zG#2&jVk3jzpbunIrI|O@@qKI!dNvXm^paatfB%5!`LJUn`=ua|;y)$o=7}K}UjlV% zez?AOa(wi4s^f@ninTlCULDD~7an?csTNA~bZ27rA!%+veae62TQ1KSaEc39ux|!v zU*bpD(ObV3{jBlDxv}Ek?^>p9s32g^9FS<(G%_lIYUybb4Bz3H4~2S!0NXzMH?}R3 ziW>yUtqbUCxpUa1mE*4EKvweWvEtG6Sp|%*eW6B>B%hEwv}&~d)?eAH2*L^#&7c?u zwq>rt123DG?t`bz4J0%Fr?VQwLW~W1q<}wp;K%RTch;M@EY5#Qg^4o*b1#6Si z^@8H6!p`cs#;FqD;5l&ELa#O>+TszWP5tCmGniB8feHV_j$`=)JVxZb*Os| z^CN8*>I3?y@?JDuTM+!dHSUNs%V#J z%iGD)Yk8r2RY(~BoeNlf*^M_KE29>=PH8^2~S z_1asv&{&N_kI{Oqw-TcYbSg6|-lKk)IhQ~YVq7Un>^w*E=)@<>;`b)Ba-&haS_}Ee zNS)5k;o2T0nxu*H3%pUh=QDC2Pw^?*PV{l#QevP2K@KhbR%{j8fKECbTwo^vM$dkj z(xiCdQg=n)a^m*?g!||DunQr(F?o^K!oi=Aef9w}ljx6JvXqQ_S_9!1`6hJdF1KEF zuBdt^mQgBcYXwew{Z0(!bWiw^HhuR&1jBO*fw2+9%CX znKCc6TrJ6(VZ;BA3xNUOdI3&#(6|am9F4WgOdbc?u=5|pL*bg;jcX3n?&W)k1Sl!j+pU_hzRx&r4oq1z;sJXfz5e#R(}xqP zkKgc}lWueGEDnSlG`H`y6^rBESi=gf45O^7R49=J-DK*=Ah95E#bV#$Q$7cyKH=pe zcU6&nz7pxbBz@qPkThC3&{1w%N5-xtt~51P^l{9bB18#W>NALBD{%Su_AxV8?IMU+ z;BxSTuczzfdBbEs|4h3351?}m6d^<&2LMhaj_7+R@KUmWV?IVF{$D7ccT;1j?N=7A zbF+Z-a=OdA4%RkQNUWBB_AmghXmn*maI zg!>!tk{ZST)1@$*k>{3n;)*E{ZebE5#Z6?0-cMB7}$< zH+Cd``FIjLiXVlw>I*?jnG|Hx$x)G1gkc;v?)^>eigR>^QQp(J7Tv7)T=`{-5-pz{ zH42oi*1iTehrh2$h-Uw%^l0RDAokd8OBmiPK67s>eSS0c;nbV_+GbG8}G^#}QzZ-4V^ zlJt~KD4_9N3}v3Umm6=hqSzlypHDf2>H9|*$TGAEBFnHdhGhr0 z|7J!?ki`(UC@oYMMO}n^jy+MTQYGcu3LJFQ(Ff6sjDDc30tz70xd`lfPKK7(&m`Y- zKvlnaKJYII$=`q8&=>iH8-yCGjzPr1%IW%B2l|>E_2PM(T>i`mLcX-c5OLEG4-4UF zkXoMzo7W7dr8)1SVGfw^sDR~#D7^H=H|_&g&tF1IOzaMEvz^f2-j4D7^%>)+y}h$D22)6Qxv5 z*AIsZPK@2ji~mLiz~!!c$KRr;*&k5!khdTX;JM8YYxJ3*5RYti$Y$B0r>>f>hDPL{ z)oX+PcCi-fa&lensTiQuDS{qA1~vD$L-x=6QKW=9-;{75xJm!Q`ucWD+vGjpc`Rp7 zFcdB#TbOW}EO=lXD|i7DACE$^kJheR9|^`Y5{U9Qh~7{gaJJDr;1HugTj||>ED3lS z4^!?~n|b-;5T=lUNOKedg@Y@U$z4LQKxlK=qDN z^qrlqyNn|Sv=@UuyL7qHfD`I_-c*{3%Yic3NY;*5$w+&K3v~8l*DLq)c8LpoEey#8 zt^nTUhhQ{{&b?-n3S9A%0-@35HkK#g>4Y2JLb9n`5xN8N1$HWyJOTSNTjwb@Vu+!Q zKtvQ6NQE-kQP-|9c}D(}SN>xkpm!i?yw*wIewK)NwGGiNz8nW?R1!&6Lz<@~8)cn) zm2$D8B_Uj?D_D!fH*NR@lGe0OIu@d-z{-Xmdpd%E*T7VeA*1c0n5FWM{&kZ+JXn$> zJ!@d4DIL4hkYky2sWB)Cwq-WQ)&$B2VQd(`Y@LAbSk`OJMF34|p#m3n$3Y{ja0+OQ z2UBzEzSrp)rQE|fH4u)UQk`7i{UyTEnd>7d`8J?xo*c1%mwfo=3;nvM^$107!@3Cf zzE}*AWPVKdk*<=9v2P$(>x}>a$F_h>4#L2tVCjj*rCwP`iJtL2P>np(b=L$m#>+n| z;Uydt2en|L=7G|`TW8;yorn~K)POESe`sR9d<1%LgeB?N5ER7B8^W4bcRAd+h3F&p zU)5td2S@aeiTF?u2t+~kt42;k{zK4alk`!$(k#!Rb*$$BtDxJxm`~Xq-W!Ny+415*<5*Rgyay5)9Cn((wZiotCSSA2@6r2g&wh`q%0MUR`-*B(P zie39G%#2;dK9fM$Gj~Z&=FI|We5Z`YZ;PcL2J_(_^0Sr#BLnhpGk75j?wK8)oZ&yK zTxcV@eq}Qs;Av_Bspa*fyyyl9uxuErIklXJ944<}e6Ug3Krnx;YrW!-hqqcx%2~8w zW70aG^>$-1Kl&bn0zcB+qw}@nPcm%+lQ|rGY9=ltUdygN)=7Es*&Xrkoc2HTG2$dX z=?=t7h3L<0PDwfYzG8XYMdm`;r!e{5RYmWHP?Xz_iKESd|7U=IrI4Ak8u+z z$m}Z7!VpX(4AY-dAQ88Fyu?AZ^$}i5^8S-Egkj;>Ru^rUC#Y_Gt}g+!kAp}NN8>V| zJi#u5dZCLJO-;=B;&ApjX;j)Z`#@hvwk;Jko%7aGoOx6PB8D-0#5t7vc=L~?h@Y(0 z-@Alydq52V@dmSsfU*Po_h&+7OkJMSC2iw5Z#SaSjwmxO^fPCX{j&xX%Eu8Mhwl#j zTqdmgspCdSvL=|lvv^f1=&U6nZ#Or>p+hgo5wbQ4x>rVC4OE9jS6*j5?RO*iKa~b49v; zgd@`M(p`F#p%FRABzt=lG^f?kD$VstA3gtd~0K{X8O zf&#!Ub$`Wtf^INxFE;kLtw7@%G7FEHl|-vRJZMBRM^{MiKrw+w!nEay(;%M2q=<4E zSS349tf+y?TwS8I`VU>3i5l0Fa#kzt_3!c#$B{d9VwtPsz-NRN39S+jZ0>o0WS7%`{;=j_o3t7dU@g>lfw*!KF*nfLL&slynzapPCs zpTuy7L004Xr_NrTH(AeIu4cjSp$sBiJLqjLyfS-m3$P<2igwXTy^dGvE`RCgDZw7@ zWXsND4lVm7rrqAcqAGz|KGmAXCbmo^7BAX+!%fjCr$$l2B$C)783yiOJK7Z7(V|vM zg+p}zGM4>@bi=ggD3X$i0A=emX=V`!x2+BcZ<*dQx7QSt2AMnATY&qK1BJJk-v;We z4^aTi-OBxtB;KNcM{3W;^fi;WPQ)JSwAqatv`>n$bq4tzfS_M)hJyz%cp!X-QSn3W zKfxoab8Z%o03kU@*(6HRza2K>O?j`uu`cDF$>XFIgD!M4xtTYO*5eNZw5R-|N#uSF zS!}hNgllQy@Xsvae>l=O0i<`u{jiTm7%uX?tL&d3srf#S2#L_rkV$&dvL%BT%gHaTc4#Tqj;x*VWMBYrB#HdjOxmC}oR4k*|MmWoNNCGj0duQ49bIpAZY$Jeocm()z zK|`;m3O?QVc5t;EkGXxd8q!KN%6i5N+&AwrH|$x+q)_JOTOU1}EUG=w3w$G&!kA(O zi3<}%y4Nh7CJgD?khqe_j!Fo6g47N`nrN3z{uEeoPw^%*<3Z$>p>yWx8NH6w0QWQ$ zju^b(xjj9mY#Pe)O8O)jg`%aNY+C#^2Ep|plOI$5@h`?_8!$nGkUVYM8%Xm{aL^;) z=S@i#rNVWb86y2Wx#~9zkgUoGG4zuvX3-GyaJE(SLdi42}3*a2e{{{!e>f9TrvBwXX;g!Xv2y5(bT+GJr^ph!}vhNC+qr z5~6hDfM8JyN(qko&;rsSAp)XOLk}$~DWM=8^Q|*8KH~fOT)*FSegA&`al9CqbI#s- z?X_3k_hQ?Am$;eGMNnsy;sW`iuB%XHpEiciHqRDY2MDqqHs#D)pu28ah1Baz<9BK& z#K=PMLT+jzS5LJ7=4K$6TmI-LJkZ}w^xjQ#4Sew7a@dIT2v(WBp`AU8 z$@*>U;2)4b;hc!lXx5M;+tlZH)s8o2LMttBl{(7x!QHc`!RK0OAi0lR-!)GOhkiNE z(kuNZCG`%Zc{WQ18UdC0Avysz3Pj=Qrf62S0T3?X*`S)FaYi>}L@pxw00EX5b$LL+ zs5_f)GerY}e+;tpEC)nkecKCzR+LTKyM9$gW0&L(Hg{oEPto<~TIl3)Wk6vLL+R*5 zgQ-Wmo_^OwZI~5%kz)cH33hoJ>YXCtx^a@Wk7R&qd~%sZLvApmhwHj;BrB0*b8B(Q z2`IM?R;U18rT%8BSVvtvdzR4U3?Ss{0$R^W)wvUS7q6}@2#SiLfUGa} zv%SboqoQk1`*e9dp&v={@d8Ip&Z+*R`c+ksixL3O`^8}$wiqPOwXiaHi)SJ{twaH@ zdpd2?*@w5`{VN~)tMt`Mu5o`})?Gm@41;h^%cl?Da;|Dxu2OXKd0_#XCv&#x(D-)M z3t0BA{p)LDm(M9CKpTg790JYUqDt_nbRk%YLgq*iKL@zz*?=CpmvgIt0Ak=4E>H(R z*3CZvve1pRKxPGgLce0Yo1gyq?nD!XZb>53UyRcL6E{gE-lH^LO!N?_YC$=_2oi}` z@G03TRpQnI8nOYP2KFPB&MT{`fo5sdZN|}=ND(3uUv<@Kk4Kc;i3-1>D%Vu(^;=x7 z2Vo)TmjM=KdLIHfX5b$iE8Eq^==rWaJ`LRP&@@6Iyh za`8QjRc}A~1oWicm>j66(5gvK(_zE;g2m7;ezS76ZytdtFHH4K4lAyI-~N?N92Xf@ zK8q;ejJJ9;@zze5B=64+?Xjb4NB)Gm;GKn?7$ZHxLQ=S=3=zy4kNMKfkn5>t#MIfO zdc2yy1+_W>F6spCB}a}F9#xtVB;5Wj1%T0q*Kr5kuU<16_6!9Dq{1+c>P>r7382fZ z;vvNu9;mre!|`&l8zHWksVstS@gGkv5Kjn7&1QFcSRulVdCW%Rg^kJOstWicm)H$- zHw39#RqDvBe1ZtJHAi~rUjPE(eK5eTK1De)aS{{!$h6z;zk)d=Ma)eDRSpOd=u;;W z2cfo;S*qlfmg>GxvZ{ItL`(EOJ7@j>WY1Ojv#_{b%OQ0c7IVIA5<2XX*<=jCa;$=k|Z;C2j!_1%s`( zdr2f&7&Rn%btbmykKn3J7(3)Bew6T0hU}K0;Y~L0Kl?9#tqC^^N|W_qCobXt6JWz= zz4Zj1Ow4l+29aCjHg@E@S6)b@o zC@%FtIyR&dR2}?M*{*bCQi!!Fj8m{6MJ1^!|D2l z7kmWSXgGWs?Sh_4cu_Oym!)Vd~6#R$2<&1d4 z@$YOye80ZmBKUf`=}Prb2U(4Ux5L~R^vTQZCI!0hBNPFW-W{OICbuzLs?19mZBTgC z%-?Rs0%zFER#f%TsI{NJL7#`GSG7cxl3FdB(A?z1iGrO2)iHUqo3BK7L5bAx3BvaW zKfeH*Ff#ClZ&b&(+*mi03e}ygtdTYL#l){88#Fv0d8-~tIkF8|L-mH)dO2MJlCbX| z$JD%vmEUluYF6CiSn$JgYg1c--&YeS9xgQZVZCl2jnF4*E}9DTd5Q+8x?pE`c*4bU zu_Cv04{5}3C$uM}@k2`pv#7z8r?#kVm-s1R7*RwUJ_!AIh%|+#lzBzL5pC3cXoCLld&Jt(xDX33+`_7q58|F3A z@HJIzCJ;~Ow%s)TxxfD-XT`gjLcN@N!iB8JaVy!Xsf~A z`VZzsgcFW%k#2|Py#-srDJ9okm$U>l zsHNW!XE%WF)0uh96rrS835SS%gHh8E8$otV{>_X@aj$@!EFzU?JD3!_)NazD%lj4X znVYHV&bwh*5h6^>h0}cdkVA0fcUz)+_-C(q>XO&!u6(vy6K^Q z45>`>2pqXpU0J1U#D|=MpN@BDm+tsyBW$8L4H;j4%%hKdnV#1nelUL*0_J^rP!^u4 z0sW1{*w(N3iRnoRW7}s}25RpYeLPpI61;2BHJ6N!6+zzg8K*l9=}nnov}*NtayP68 z5D-nWTMK`i)juFisZ*DDO}yJpc{=fR6X9xe+73Y8^IZ4HbJ~0M-2(=Bx6;=wdJZPQ zmL)R~WuVk(PPE=4k&VrM%#oAyjKk0`CWh8RfkgXDTop_&<2z?;>bQcW{?lEmr!g@U z<;L{V7qBCK(mOkr7nZhY6cUi`GwBBT&h1I-SgCH=;#(cBQVDI&CSvo?y>^nIorAFV z=`~De_=)oEfdDWNX8*2osiaK>bJakxMVxUzF09>y%E~rP(+iuY2p&-@;v& zv>-B>>!d)O?ImUeb~!#p+MVdPF#8fJ4b;Ds2ygft#n@jcb1FA~ zE%y8iD~9~6TL&VXSp&@71gWtZ%xw?qdqM@352Cys~S4g^Rfs6 zM`0tG5MmM}v{=4vNsT114?y#Za`gfYMm=PPLVO1e@@*ahGOZX;qw!r(m8n|VjNsm~ z)}SW10ij}na5YjIy?4Fc<3*h4g_1T=lTv(21&5TKHG*wKb)G$T?AQy{iFMtC1>kPX zA$%~LHU#`wYmS9#Yqt4GomB0_q9>^V?-d+`Rfv~VIfon-;^l{s-`O?4QJtlJffX*b zA+IMNW@koz_~~h_Pv7v1S2(alSQoJU;%3Q(QKJJ!tKc2}V`au4=O9iJ4bh$xKJ9+1 zpLvq5_LXRfZ0e)x(wvME2LRwj7g|)G^nH@n5u+WQE^R|P&8~69t-LB|5p#KB-C5)_ zzX&x1botND{i3p#|Bvtm(8Nw#tOYJZUezX`=A+)S{%62SGEB`E%-w6gNy`!(gqtaZ zm~$)uLfes8P=)LzA3$>=;AN(jyllwB#vw(7D;7fTO2SPmkX~Y4>HJ>e2l4vJms;PI z8ZJMPW_wP&=q5-TJV!43=Dc#6;_hk@FTzc6FSL*glaCEOp(ov;vf$B&1%<9@GvR?1 z`0LK~$0Z7v?x2&XS|GX0dAF@BYTMoc?^$&TC>}19z2pJ##b_X=!~10WH`}FL-j6dj z15`D8vzyqwABR2)+nGf_UcxFg=CH+?`fUheLqpgIs`=JXJB0t8YlHFC_`%;5=%#Ju4 zRoomSB&<(s?6;-4YyvoW%TjC>z}t5|wlJCSd$Ma5;w{@ov0qD}mM@8EBAyvEh@1CGehEppKO2y}WO5B+x?q}jv&x#Z zm}-CZhw2Z!E`%KL`A@uO?!+8*vB_S$dNNAvg5ATUo_X;I9}|X<1%NdzITK4I)}~-c|=GOFAuK&aYe`1qpI-2bLR=Kl%4=U&(AA_D^tE-vFN_;`Ss~B z!n*UzAoMV@u;T|-_rw!2jBh93R}(AxNI}@+-~;Jz*VrIsVfnIq1yyIBVUqd{4&1kS zYrGwG!HjEyVCS7R`R(E0KB8ez05wU&u2Y*7xJd_83v$e6&Oy_{9h`?d{N@0-U;_kY z;{z6*l72mqht;&J@T)~KARN+HC*aco&#woN9$7njrd?MF^HUe3ZFuQEFWT*WwuyHC zF~Qra#{!f&xC6GHp&Jc%-%`Eh8!L?sB?Hwbs_+y+K}NxF+#Y2rX1YQ~F(w(7_{%x$ z4y9IhwX*9*d*?mvr(*`}q(uCJ4?LH!_*>I1r$c06+EWWo;Sx{jAR; zJu_5<<_tq92MitV-rxAiXlq6oYlY^IVwgo)Hg21-~D zObJE9vzOxZ1z*l=+RiP9Zyts4s!VZNfSN^Uw)sd94{Q6*M^7h1?9&IY?phz4A3QYg zRctq@ZPi>R*@Mqqa~&daILGcxk;O&sxW2t%c;%r!QHI_@pORef+7CmPL)!i)B6_#w zj+*pBY|P2iQLumjam&8N#l_6zM~@!$MhIxQW^}SSj_DUu+QCp)CVQmaAVr1Wvj!bP zn@U9VGnJocq;}dwYG<{d;&8~2u6YN;)jCsljyM57#^$qrV4K*Mom;Pj-=C9@o+%$$ za9&x9s~c#Y+>w0~Jxj41@8ap%9TE~Ud4x8~P<>ZhU~P+*cg|E@l=xK5y`5*p8u<11 z#l*$U#ufNDC$X|`TmI6LcJJQ3at4l1H3bn2KsL-m&IwzqVSy8*yyx2cuD2zRb`nEJ z@3fG?X8A6DtCaU%JXjL&*t!@(<*3L=t^`HDsRVhi9eoRL0=G>;gQez?bNc#yz~i%R z+6v*p$%v3!qdt|Ye^$86k5EnsU;hr-q24!dlztQ=tE+t*R&}3knGkWEwa4>0nv>%y zOuT$DamkH->)*S+?yi6jBxKg)`|Ow$neaRqIQgIPYA7kvuG}p*zpFe^9+G2nT*;t+ zTBt)|$?HG_Wll@^u}V?Qu*P2hAII7433DImK*Q7`Jv#6qMs|$QkKmIfN<^c zGEV$*y_puaWwy4qQ-DR7`dk|(8*1QeP7p{`&V4*sLTh8kp zq4VMJ{Ztc`FY+_U62@+LbSQD!RU5OQn{sjW*Ad!+8ZKM>L!db7<9^L z*A4hO-hCK;3iZJUnrbR0<7<3%%xB&S=rqgs)o~|>PR`SbE_kXlpe$HN0Hk2lrE=xC zC@#|XeseysVDvYuO=hiY6?AuSan6$CFG1kEaBdwLBHM6Cbgkz1 zIh9FU&F$$A=O56ciuEj&YFasY=`W-7ej{i68lJ3;g{ji$P(zM6hXeOX%pKZ?l4tl? zlL0|rHR&%jB&(E^!UNhpEPKi6A|ah4HWH=DmPPiQQM9PK1v-qShe!cYAEjtu0kfDs z!W}DINt%w$(BUbGDYTFDwxlw2O*6$U4O%0?7w)=5OND5^i(>BoVBI4XmdBOUgeSH? zFw3LT-lEQRwz#I6g59x_=ho@K?W*G*QYtb)>+H=*o zCY9~O6HDN+4HPbvu^f)$#^N$xc`CdXuFihS;}p(O;;-`tZ8hyR^X0VS2sr?A0fcix zNxW5M!*qT?icy`qZAHqwr%Za@&u-hc%|gm{Qp%#|==h|aJ+&q=RCB`sJC4q+V5F6x zMXl($lMrt=Kx4Ji)Lz{%i+@{lIw;sQ(7GJk5C@OC5EBjOSN$PVX z&qx!xhUBucaBdhVjuxcrtg}Q2$?e>PVZfw8y_3z0XEx!fVM87i>g{GE@c94Nv)kCM zgZ7M^!xop88o!rEh3%I4#91wmdcOAI4Dx;$HIOxl$@8(q;#QKcbpxD+)*Eld$YVPA zfT9E48dlI-q?n}U8CK7T^^3QwC@-J#gm@229qF&=p;wB`dngQa*< zhpx?(D*D(D=vL}SoKa61oK8N31XWUhPoYD@K2+6~ruzEDEzH)QWbQL(=-`mMk4s&G z90fu4s33Sf1{8PHe~xS$vP2CWRbE!(M`ONL^n+0lnYHv}kXQf>ruFD}oeM*vZYj?TJQzNo}Mk|9H<%Vm96YuDwp zE?CW?qVuP*GSbo`Vd~h_>U!e4mGFkEVub`uzEIXf)l?s53v~(3C2s@W8s-eu`s&hW z-_^yz0JSBP0|c#bbguTLzkb%h9AMrhg^M)PuHbxc%wv07NWFDvz4&qAM49he_ipR7 zxZR78#9!ZoNlH6N)P1A=Xy}Fd*!@1sK^7L4DWF!rFnY9m14kA*kwVdL-MPY#Fi$vk z&C&4zJnq6jkF#e1Ylr^0y1^7A=RfSW4x*OzT`elykF)DBy6Z`t+i})+d^lqxBd7HA z^n|;0;8%wZ;>mIe!YV^II%Jt>57g;Do+bs$;k0OE3Sy+lQ-&@?j<}&$56?b0vCI@J zZd(3gKko69CkwUwINeKRsbhru!*LCf2S*^fHjlL2* API Keys +Create an API Key with `request:receive` scope in Overmind under **Settings › API Keys**. -![account settings](account_settings.png) -![api key](api_key.png) +![User settings menu in the sidebar](account_settings.png) +![API Keys settings page](api_key.png) Install the source into your Kubernetes cluster using Helm: From 2e7a4321d50412d7453b3e4193c2ce6971239046 Mon Sep 17 00:00:00 2001 From: David Schmitt Date: Tue, 3 Mar 2026 16:04:28 +0100 Subject: [PATCH 43/74] Go library updates (#4083) Updates Go dependencies `cloudflare/circl` to `v1.6.3` and `refraction-networking/utls` to `v1.8.2` to resolve Dependabot alerts (ENG-2928). ---

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�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 diff --git a/docs.overmind.tech/docs/sources/aws/configuration.md b/docs.overmind.tech/docs/sources/aws/configuration.md index 9d2c115a..329d3763 100644 --- a/docs.overmind.tech/docs/sources/aws/configuration.md +++ b/docs.overmind.tech/docs/sources/aws/configuration.md @@ -10,15 +10,15 @@ To be able to analyse and discover your infrastructure, Overmind requires read-o ## 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) +To create an AWS source, open [Settings](https://app.overmind.tech/settings) by clicking your avatar in the sidebar, then navigating to [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) +![User settings menu in the sidebar](./account_settings.png) -Then click Add Source > AWS. +Click **Add source** and select **AWS**. -![Screenshot of the sources subsection of the Overmind settings with the Add Source > AWS button highlighted](./aws_source_settings.png) +![Sources settings page with Add source popover](./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". +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) @@ -111,7 +111,7 @@ At this point the permissions are complete, the last step is to copy the ARN of ## 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. +After you have configured a source, it'll show up in [Settings › Sources](https://app.overmind.tech/settings/sources). There you can check that the source is healthy. ## Explore your new data diff --git a/docs.overmind.tech/docs/sources/aws/configure-aws.png b/docs.overmind.tech/docs/sources/aws/configure-aws.png index e2548560782e044e39fd8df866da3c10cc2154a5..c88e146f0b342254263bfa5fd65f01e2fa7a1767 100644 GIT binary patch literal 168857 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