diff --git a/.github/workflows/source_alicloud.yml b/.github/workflows/source_alicloud.yml new file mode 100644 index 00000000000000..2e3d901e2886de --- /dev/null +++ b/.github/workflows/source_alicloud.yml @@ -0,0 +1,85 @@ +name: Source Plugin Alibaba Cloud Workflow + +on: + pull_request: + paths: + - "plugins/source/alicloud/**" + - ".github/workflows/source_alicloud.yml" + push: + branches: + - main + paths: + - "plugins/source/alicloud/**" + - ".github/workflows/source_alicloud.yml" + +jobs: + plugins-source-alicloud: + timeout-minutes: 30 + name: "plugins/source/alicloud" + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./plugins/source/alicloud + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 2 + - name: Set up Go 1.x + uses: actions/setup-go@v3 + with: + go-version-file: plugins/source/alicloud/go.mod + cache: true + cache-dependency-path: plugins/source/alicloud/go.sum + - name: golangci-lint + uses: cloudquery/golangci-lint-action@master + with: + version: v1.50.1 + working-directory: plugins/source/alicloud + args: "--config ../../.golangci.yml" + - name: Get dependencies + run: go get -t -d ./... + - name: Build + run: go build . + - name: Test + run: make test + - name: gen + if: github.event_name == 'pull_request' + run: make gen + - name: Fail if generation updated files + if: github.event_name == 'pull_request' + run: test "$(git status -s | wc -l)" -eq 0 || (git status -s; exit 1) + validate-release: + timeout-minutes: 30 + runs-on: ubuntu-latest + env: + CGO_ENABLED: 0 + steps: + - name: Checkout + if: startsWith(github.head_ref, 'release-please--branches--main--components') || github.event_name == 'push' + uses: actions/checkout@v3 + - uses: actions/cache@v3 + if: startsWith(github.head_ref, 'release-please--branches--main--components') || github.event_name == 'push' + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-1.19.4-release-cache-${{ hashFiles('plugins/source/alicloud/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-1.19.4-release-cache-plugins-source-alicloud + - name: Set up Go + if: startsWith(github.head_ref, 'release-please--branches--main--components') || github.event_name == 'push' + uses: actions/setup-go@v3 + with: + go-version-file: plugins/source/alicloud/go.mod + - name: Install GoReleaser + if: startsWith(github.head_ref, 'release-please--branches--main--components') || github.event_name == 'push' + uses: goreleaser/goreleaser-action@v3 + with: + distribution: goreleaser-pro + version: latest + install-only: true + - name: Run GoReleaser Dry-Run + if: startsWith(github.head_ref, 'release-please--branches--main--components') || github.event_name == 'push' + run: goreleaser release --snapshot --rm-dist --skip-validate --skip-publish --skip-sign -f ./plugins/source/alicloud/.goreleaser.yaml + env: + GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} diff --git a/.github/workflows/wait_for_required_workflows.yml b/.github/workflows/wait_for_required_workflows.yml index 1d84651b4ce461..fa3d4205823ef2 100644 --- a/.github/workflows/wait_for_required_workflows.yml +++ b/.github/workflows/wait_for_required_workflows.yml @@ -41,6 +41,7 @@ jobs: } let actions = ["cli", "scaffold", + "plugins/source/alicloud", "plugins/source/aws", "plugins/source/azure", "plugins/source/azuredevops", diff --git a/plugins/source/alicloud/Makefile b/plugins/source/alicloud/Makefile index 915b9c1ae0f67c..c869da62ed16a1 100644 --- a/plugins/source/alicloud/Makefile +++ b/plugins/source/alicloud/Makefile @@ -30,6 +30,7 @@ gen-mocks: gen-docs: rm -rf ./docs/tables/* go run main.go doc ./docs/tables + sed 's_(\(.*\))_(https://github.com/cloudquery/cloudquery/blob/main/plugins/source/alicloud/docs/tables/\1)_' docs/tables/README.md > ../../../website/pages/docs/plugins/sources/alicloud/tables.md .PHONY: build build: diff --git a/plugins/source/alicloud/client/resolvers.go b/plugins/source/alicloud/client/resolvers.go index cb3b8444e9fa8c..87a6f04ccccc7e 100644 --- a/plugins/source/alicloud/client/resolvers.go +++ b/plugins/source/alicloud/client/resolvers.go @@ -2,8 +2,10 @@ package client import ( "context" + "time" "github.com/cloudquery/plugin-sdk/schema" + "github.com/thoas/go-funk" ) func ResolveAccount(_ context.Context, meta schema.ClientMeta, r *schema.Resource, _ schema.Column) error { @@ -15,3 +17,17 @@ func ResolveRegion(_ context.Context, meta schema.ClientMeta, r *schema.Resource client := meta.(*Client) return r.Set("region", client.Region) } + +func TimestampResolver(layout, path string) schema.ColumnResolver { + return func(_ context.Context, meta schema.ClientMeta, r *schema.Resource, c schema.Column) error { + s := funk.Get(r.Item, path, funk.WithAllowZero()).(string) + if s == "" { + return r.Set(c.Name, nil) + } + t, err := time.Parse(layout, s) + if err != nil { + return err + } + return r.Set(c.Name, t) + } +} diff --git a/plugins/source/alicloud/docs/tables/alicloud_ecs_instances.md b/plugins/source/alicloud/docs/tables/alicloud_ecs_instances.md index 66e87f3fdfa524..32c51c4b0d2af1 100644 --- a/plugins/source/alicloud/docs/tables/alicloud_ecs_instances.md +++ b/plugins/source/alicloud/docs/tables/alicloud_ecs_instances.md @@ -16,12 +16,12 @@ The composite primary key for this table is (**account_id**, **region_id**, **in |hostname|String| |image_id|String| |instance_type|String| -|auto_release_time|String| -|last_invoked_time|String| +|auto_release_time|Timestamp| +|last_invoked_time|Timestamp| |os_type|String| |device_available|Bool| |instance_network_type|String| -|registration_time|String| +|registration_time|Timestamp| |local_storage_amount|Int| |network_type|String| |intranet_ip|String| @@ -37,7 +37,7 @@ The composite primary key for this table is (**account_id**, **region_id**, **in |gpu_amount|Int| |connected|Bool| |invocation_count|Int| -|start_time|String| +|start_time|Timestamp| |zone_id|String| |internet_max_bandwidth_in|Int| |internet_charge_type|String| @@ -62,10 +62,10 @@ The composite primary key for this table is (**account_id**, **region_id**, **in |description|String| |recyclable|Bool| |sale_cycle|String| -|expired_time|String| +|expired_time|Timestamp| |internet_ip|String| |memory|Int| -|creation_time|String| +|creation_time|Timestamp| |agent_version|String| |key_pair_name|String| |hpc_cluster_id|String| diff --git a/plugins/source/alicloud/docs/tables/alicloud_oss_bucket_stats.md b/plugins/source/alicloud/docs/tables/alicloud_oss_bucket_stats.md index 55545ecf2d1e45..5c759b2aa1fab0 100644 --- a/plugins/source/alicloud/docs/tables/alicloud_oss_bucket_stats.md +++ b/plugins/source/alicloud/docs/tables/alicloud_oss_bucket_stats.md @@ -1,6 +1,6 @@ # Table: alicloud_oss_bucket_stats -The primary key for this table is **_cq_id**. +The composite primary key for this table is (**bucket_name**, **account_id**). ## Relations @@ -12,14 +12,16 @@ This table depends on [alicloud_oss_buckets](alicloud_oss_buckets.md). | ------------- | ------------- | |_cq_source_name|String| |_cq_sync_time|Timestamp| -|_cq_id (PK)|UUID| +|_cq_id|UUID| |_cq_parent_id|UUID| +|bucket_name (PK)|String| +|account_id (PK)|String| |xml_name|JSON| |storage|Int| |object_count|Int| |multipart_upload_count|Int| |live_channel_count|Int| -|last_modified_time|Int| +|last_modified_time|Timestamp| |standard_storage|Int| |standard_object_count|Int| |infrequent_access_storage|Int| diff --git a/plugins/source/alicloud/docs/tables/alicloud_oss_buckets.md b/plugins/source/alicloud/docs/tables/alicloud_oss_buckets.md index 38adcf8c95c94b..8cad5516e5e850 100644 --- a/plugins/source/alicloud/docs/tables/alicloud_oss_buckets.md +++ b/plugins/source/alicloud/docs/tables/alicloud_oss_buckets.md @@ -1,6 +1,6 @@ # Table: alicloud_oss_buckets -The primary key for this table is **name**. +The composite primary key for this table is (**account_id**, **name**). ## Relations @@ -15,6 +15,7 @@ The following tables depend on alicloud_oss_buckets: |_cq_sync_time|Timestamp| |_cq_id|UUID| |_cq_parent_id|UUID| +|account_id (PK)|String| |xml_name|JSON| |name (PK)|String| |location|String| diff --git a/plugins/source/alicloud/resources/services/bss/bill_fetch_mock_test.go b/plugins/source/alicloud/resources/services/bss/bill_fetch_mock_test.go index f93affbd4fdaf2..bd215b5a383158 100644 --- a/plugins/source/alicloud/resources/services/bss/bill_fetch_mock_test.go +++ b/plugins/source/alicloud/resources/services/bss/bill_fetch_mock_test.go @@ -19,7 +19,7 @@ func buildBssBill(t *testing.T, ctrl *gomock.Controller) client.Services { } b.Success = true b.Data.TotalCount = 2 - mock.EXPECT().QueryBill(gomock.Any()).Times(2).Return(b, nil) + mock.EXPECT().QueryBill(gomock.Any()).AnyTimes().Return(b, nil) return client.Services{BSS: mock} } diff --git a/plugins/source/alicloud/resources/services/bss/bill_overview_fetch_mock_test.go b/plugins/source/alicloud/resources/services/bss/bill_overview_fetch_mock_test.go index 8aebd4730edc1a..7388269f2e619c 100644 --- a/plugins/source/alicloud/resources/services/bss/bill_overview_fetch_mock_test.go +++ b/plugins/source/alicloud/resources/services/bss/bill_overview_fetch_mock_test.go @@ -18,7 +18,7 @@ func buildBssBillOverview(t *testing.T, ctrl *gomock.Controller) client.Services t.Fatal(err) } b.Success = true - mock.EXPECT().QueryBillOverview(gomock.Any()).Times(1).Return(b, nil) + mock.EXPECT().QueryBillOverview(gomock.Any()).AnyTimes().Return(b, nil) return client.Services{BSS: mock} } diff --git a/plugins/source/alicloud/resources/services/ecs/instances.go b/plugins/source/alicloud/resources/services/ecs/instances.go index 0a23ca36f55d11..fa77b66dfe6a30 100644 --- a/plugins/source/alicloud/resources/services/ecs/instances.go +++ b/plugins/source/alicloud/resources/services/ecs/instances.go @@ -5,6 +5,9 @@ import ( "github.com/cloudquery/cloudquery/plugins/source/alicloud/client" "github.com/cloudquery/plugin-sdk/schema" "github.com/cloudquery/plugin-sdk/transformers" + + "reflect" + "strings" ) func Instances() *schema.Table { @@ -18,6 +21,18 @@ func Instances() *schema.Table { transformers.WithPrimaryKeys( "RegionId", "InstanceId", ), + transformers.WithTypeTransformer(func(f reflect.StructField) (schema.ValueType, error) { + if strings.HasSuffix(f.Name, "Time") { + return schema.TypeTimestamp, nil + } + return transformers.DefaultTypeTransformer(f) + }), + transformers.WithResolverTransformer(func(f reflect.StructField, path string) schema.ColumnResolver { + if strings.HasSuffix(f.Name, "Time") { + return client.TimestampResolver("2006-01-02T15:04Z", path) + } + return transformers.DefaultResolverTransformer(f, path) + }), ), Columns: []schema.Column{ { diff --git a/plugins/source/alicloud/resources/services/ecs/instances_fetch_mock_test.go b/plugins/source/alicloud/resources/services/ecs/instances_fetch_mock_test.go index a7e4b939e7e234..c0b96910867a00 100644 --- a/plugins/source/alicloud/resources/services/ecs/instances_fetch_mock_test.go +++ b/plugins/source/alicloud/resources/services/ecs/instances_fetch_mock_test.go @@ -22,6 +22,12 @@ func buildEcsInstances(t *testing.T, ctrl *gomock.Controller) client.Services { t.Fatal(err) } b.BaseResponse = fakeSuccessRequest(t) + b.Instances.Instance[0].CreationTime = "2020-01-01T01:23Z" + b.Instances.Instance[0].StartTime = "2020-01-01T01:23Z" + b.Instances.Instance[0].ExpiredTime = "2020-01-01T01:23Z" + b.Instances.Instance[0].RegistrationTime = "2020-01-01T01:23Z" + b.Instances.Instance[0].AutoReleaseTime = "2020-01-01T01:23Z" + b.Instances.Instance[0].LastInvokedTime = "2020-01-01T01:23Z" b.TotalCount = 2 mock.EXPECT().DescribeInstances(gomock.Any()).Times(2).Return(b, nil) return client.Services{ECS: mock} diff --git a/plugins/source/alicloud/resources/services/oss/bucket_stats.go b/plugins/source/alicloud/resources/services/oss/bucket_stats.go index 64bc5343253717..02953c53e2c1a8 100644 --- a/plugins/source/alicloud/resources/services/oss/bucket_stats.go +++ b/plugins/source/alicloud/resources/services/oss/bucket_stats.go @@ -1,7 +1,10 @@ package oss import ( + "reflect" + "github.com/aliyun/aliyun-oss-go-sdk/oss" + "github.com/cloudquery/cloudquery/plugins/source/alicloud/client" "github.com/cloudquery/plugin-sdk/schema" "github.com/cloudquery/plugin-sdk/transformers" ) @@ -12,10 +15,30 @@ func BucketStats() *schema.Table { Resolver: fetchOssBucketStats, Transform: transformers.TransformWithStruct( &oss.BucketStat{}, - transformers.WithPrimaryKeys( - "Name", - ), + transformers.WithTypeTransformer(func(f reflect.StructField) (schema.ValueType, error) { + if f.Name == "LastModifiedTime" { + return schema.TypeTimestamp, nil + } + return transformers.DefaultTypeTransformer(f) + }), ), - Columns: []schema.Column{}, + Columns: []schema.Column{ + { + Name: "bucket_name", + Type: schema.TypeString, + Resolver: schema.ParentColumnResolver("name"), + CreationOptions: schema.ColumnCreationOptions{ + PrimaryKey: true, + }, + }, + { + Name: "account_id", + Type: schema.TypeString, + Resolver: client.ResolveAccount, + CreationOptions: schema.ColumnCreationOptions{ + PrimaryKey: true, + }, + }, + }, } } diff --git a/plugins/source/alicloud/resources/services/oss/buckets.go b/plugins/source/alicloud/resources/services/oss/buckets.go index 8b13babc659937..28a337bd226d20 100644 --- a/plugins/source/alicloud/resources/services/oss/buckets.go +++ b/plugins/source/alicloud/resources/services/oss/buckets.go @@ -18,7 +18,16 @@ func Buckets() *schema.Table { "Name", ), ), - Columns: []schema.Column{}, + Columns: []schema.Column{ + { + Name: "account_id", + Type: schema.TypeString, + Resolver: client.ResolveAccount, + CreationOptions: schema.ColumnCreationOptions{ + PrimaryKey: true, + }, + }, + }, Relations: []*schema.Table{ BucketStats(), }, diff --git a/plugins/source/alicloud/resources/services/oss/buckets_fetch_mock_test.go b/plugins/source/alicloud/resources/services/oss/buckets_fetch_mock_test.go index 6d5ce2b1f43553..03b1ae7cc0bb31 100644 --- a/plugins/source/alicloud/resources/services/oss/buckets_fetch_mock_test.go +++ b/plugins/source/alicloud/resources/services/oss/buckets_fetch_mock_test.go @@ -17,6 +17,7 @@ func buildOssBuckets(t *testing.T, ctrl *gomock.Controller) client.Services { if err := faker.FakeObject(&b); err != nil { t.Fatal(err) } + b.Buckets[0].Location = "cn-hangzhou" mock.EXPECT().ListBuckets().Return(b, nil) buildOssBucketStats(t, mock, b.Buckets[0].Name) diff --git a/release-please-config.json b/release-please-config.json index d3907fdd11fd53..d1f44c510182b6 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -4,6 +4,7 @@ "packages": { "cli": { "component": "cli" }, "scaffold": { "component": "scaffold" }, + "plugins/source/alicloud": { "component": "plugins-source-alicloud" }, "plugins/source/aws": { "component": "plugins-source-aws" }, "plugins/source/azure": { "component": "plugins-source-azure" }, "plugins/source/azuredevops": { "component": "plugins-source-azuredevops" }, diff --git a/website/pages/docs/plugins/sources/_meta.json b/website/pages/docs/plugins/sources/_meta.json index 18998e094c07a7..ff6516ec9c1a3b 100644 --- a/website/pages/docs/plugins/sources/_meta.json +++ b/website/pages/docs/plugins/sources/_meta.json @@ -1,5 +1,6 @@ { "overview": "Overview", + "alicloud": "Alibaba Cloud", "aws": "AWS", "azure": "Azure", "azuredevops": "Azure DevOps", diff --git a/website/pages/docs/plugins/sources/alicloud/_meta.json b/website/pages/docs/plugins/sources/alicloud/_meta.json new file mode 100644 index 00000000000000..d2ba90ccfb4341 --- /dev/null +++ b/website/pages/docs/plugins/sources/alicloud/_meta.json @@ -0,0 +1,4 @@ +{ + "overview": "Overview", + "tables": "Tables" +} diff --git a/website/pages/docs/plugins/sources/alicloud/overview.md b/website/pages/docs/plugins/sources/alicloud/overview.md new file mode 100644 index 00000000000000..46765752e0e0c5 --- /dev/null +++ b/website/pages/docs/plugins/sources/alicloud/overview.md @@ -0,0 +1,115 @@ +# Alibaba Cloud Source Plugin + +import { getLatestVersion } from "../../../../../utils/versions"; +import { Badge } from "../../../../../components/Badge"; + + + +The Alibaba Cloud source plugin for CloudQuery extracts configuration from the [Alibaba Cloud (阿里云) API](https://www.alibabacloud.com/product/openapiexplorer) and loads it into any supported CloudQuery destination (e.g. PostgreSQL). + +## Configuration + +The following configuration syncs from Alibaba Cloud to a Postgres destination. The (top level) source spec section is described in the [Source Spec Reference](https://www.cloudquery.io/docs/reference/source-spec). The config for the `postgresql` destination is not shown here. See our [Quickstart](/docs/quickstart) if you need help setting up the destination. + +```yaml +kind: source +spec: + name: "alicloud" + path: "cloudquery/alicloud" + version: "VERSION_SOURCE_ALICLOUD" + tables: ["*"] + destinations: + - "postgresql" + spec: + accounts: + - name: my_account + regions: + - cn-hangzhou + - cn-beijing + - eu-west-1 + - us-west-1 + # ... + access_key: ${ALICLOUD_ACCESS_KEY} + secret_key: ${ALICLOUD_SECRET_KEY} +``` + +- `accounts` (array[object], required): + + A list of accounts to sync. Every account must have a unique name, and must specify at least one region. The `access_key` and `secret_key` are required and can be specified as environment variables, as shown in the example above. + - `name` (string, required): A unique name for the account. + - `regions` (array[string], required): A list of regions to sync. For example, `["cn-hangzhou", "cn-beijing"]`. + - `access_key` (string, required): A valid access key for the account + - `secret_key` (string, required): A valid secret key for the account, corresponding to the access key + +- `bill_history_months` (int, optional): + + The number of months of billing history to fetch for the `alicloud_bss_bill` and `alicloud_bss_bill_overview` tables. Defaults to 12. + + +See the [Alibaba documentation](https://www.alibabacloud.com/help/en/basics-for-beginners/latest/obtain-an-accesskey-pair) for how to obtain an AccessKey pair. + +## Example Queries + +### Find all ECS instances in a region + +```sql +select + instance_id, + os_name, + region_id, + start_time, + tags +from + alicloud_ecs_instances +where + region_id = 'eu-west-1'; +``` + +```text ++------------------------+--------------------------------------+-----------+-------------------+---------------+ +| instance_id | os_name | region_id | start_time | tags | +|------------------------+--------------------------------------+-----------+-------------------+---------------| +| i-xxxxxxxxxxxxxxxxxxxx | Alibaba Cloud Linux 3.2104 LTS 64位 | eu-west-1 | 2023-01-17T14:40Z | {"Tag": null} | ++------------------------+--------------------------------------+-----------+-------------------+---------------+ +``` + +### Query past bills + +```sql +select + product_name, + item, + pip_code, + currency, + adjust_amount +from + alicloud_bss_bill_overview; +``` + +```text ++------------------------+----------------+----------+----------+---------------+ +| product_name | item | pip_code | currency | adjust_amount | +|------------------------+----------------+----------+----------+---------------| +| Object Storage Service | PayAsYouGoBill | oss | USD | 0.0 | ++------------------------+----------------+----------+----------+---------------+ +``` + +### Query bucket stats + +```sql +select + account_id, + bucket_name, + object_count, + storage +from + alicloud_oss_bucket_stats; +``` + +```text ++------------+-------------+--------------+---------+ +| account_id | bucket_name | object_count | storage | +|------------+-------------+--------------+---------| +| test | cq-test | 2 | 29665 | ++------------+-------------+--------------+---------+ +``` \ No newline at end of file diff --git a/website/pages/docs/plugins/sources/alicloud/tables.md b/website/pages/docs/plugins/sources/alicloud/tables.md new file mode 100644 index 00000000000000..0fb42bf98cee02 --- /dev/null +++ b/website/pages/docs/plugins/sources/alicloud/tables.md @@ -0,0 +1,9 @@ +# Source Plugin: alicloud + +## Tables + +- [alicloud_bss_bill](https://github.com/cloudquery/cloudquery/blob/main/plugins/source/alicloud/docs/tables/alicloud_bss_bill.md) +- [alicloud_bss_bill_overview](https://github.com/cloudquery/cloudquery/blob/main/plugins/source/alicloud/docs/tables/alicloud_bss_bill_overview.md) +- [alicloud_ecs_instances](https://github.com/cloudquery/cloudquery/blob/main/plugins/source/alicloud/docs/tables/alicloud_ecs_instances.md) +- [alicloud_oss_buckets](https://github.com/cloudquery/cloudquery/blob/main/plugins/source/alicloud/docs/tables/alicloud_oss_buckets.md) + - [alicloud_oss_bucket_stats](https://github.com/cloudquery/cloudquery/blob/main/plugins/source/alicloud/docs/tables/alicloud_oss_bucket_stats.md) \ No newline at end of file diff --git a/website/pages/docs/plugins/sources/overview.mdx b/website/pages/docs/plugins/sources/overview.mdx index b1cd57d2af9827..5a2d96e38bd131 100644 --- a/website/pages/docs/plugins/sources/overview.mdx +++ b/website/pages/docs/plugins/sources/overview.mdx @@ -16,31 +16,32 @@ Official source plugins follow [release stages](#source-plugin-release-stages). type="source" plugins={ [ - { name: "AWS", stage:"GA" }, - { name: "Azure", stage:"GA" }, - { name: "GCP", stage:"GA" }, - { name: "DigitalOcean", stage:"GA" }, - { name: "Fastly", stage:"Preview" }, - { name: "GitHub", stage:"GA" }, - { name: "GitLab", stage:"Preview" }, - { name: "Hacker News", stage:"Preview", id: `hackernews` }, - { name: "Heroku", stage:"Preview" }, - { name: "k8s", stage:"Preview" }, - { name: "Okta", stage:"Preview" }, - { name: "Oracle", stage:"Preview"}, - { name: "Terraform", stage:"Preview" }, - { name: "Cloudflare", stage:"Preview" }, - { name: "Gandi", stage:"Preview" }, - { name: "Datadog", stage:"Preview" }, - { name: "Salesforce", stage:"Preview" }, - { name: "Slack", stage:"Preview" }, - { name: "Shopify", stage:"Preview" }, - { name: "Stripe", stage:"Preview" }, - { name: "PagerDuty", stage:"Preview"}, - { name: "Tailscale", stage:"Preview" }, - { name: "Vercel", stage:"Preview" }, - { name: "Snyk", stage:"Preview" }, - { name: "Azure DevOps", stage:"Preview", id: `azuredevops` }, + { name: "Alibaba Cloud", stage: "Preview", id: `alicloud` }, + { name: "AWS", stage: "GA" }, + { name: "Azure", stage: "GA" }, + { name: "GCP", stage: "GA" }, + { name: "DigitalOcean", stage: "GA" }, + { name: "Fastly", stage: "Preview" }, + { name: "GitHub", stage: "GA" }, + { name: "GitLab", stage: "Preview" }, + { name: "Hacker News", stage: "Preview", id: `hackernews` }, + { name: "Heroku", stage: "Preview" }, + { name: "k8s", stage: "Preview" }, + { name: "Okta", stage: "Preview" }, + { name: "Oracle", stage: "Preview" }, + { name: "Terraform", stage: "Preview" }, + { name: "Cloudflare", stage: "Preview" }, + { name: "Gandi", stage: "Preview" }, + { name: "Datadog", stage: "Preview" }, + { name: "Salesforce", stage: "Preview" }, + { name: "Slack", stage: "Preview" }, + { name: "Shopify", stage: "Preview" }, + { name: "Stripe", stage: "Preview" }, + { name: "PagerDuty", stage: "Preview"}, + { name: "Tailscale", stage: "Preview" }, + { name: "Vercel", stage: "Preview" }, + { name: "Snyk", stage: "Preview" }, + { name: "Azure DevOps", stage: "Preview", id: `azuredevops` }, ] } />