Skip to content

Add Datadog credential issuance - #40

Open
santhosh-c1 wants to merge 29 commits into
mainfrom
santhosh.kumar/credential-issuance
Open

Add Datadog credential issuance#40
santhosh-c1 wants to merge 29 commits into
mainfrom
santhosh.kumar/credential-issuance

Conversation

@santhosh-c1

@santhosh-c1 santhosh-c1 commented Aug 19, 2026

Copy link
Copy Markdown

Summary

  • Issue Datadog service account application keys, deterministically named from the C1 request id.
  • Add revocation for issued application keys through the service-account key API.
  • Sync organization API keys and service account application keys as distinct secret kinds, and keep delete for organization API keys.
  • Advertise credential issuance only when secret sync is enabled.
  • Add an opt-in live smoke test for issue, authentication, and revoke.

Mapping

Issuance targets a service account, not an organization API key. An org-scoped key issued "on behalf of" a selected user is not an honest record of who holds it, so Issue re-checks that the selected Datadog user is still a service account, refuses a human user, and mints a key owned by and scoped to that service account. Issue is also non-duplicating: it looks the deterministic request name up first and refuses rather than minting a second key, because Datadog cannot re-return plaintext material.

Revocation needs the owning service account as well as the key — Datadog has no delete-by-key-id-alone form for these keys. It travels in the resource deleter's existing parentResourceID parameter rather than a packed composite handle, so the handle stays a bare provider id like every other secret this connector syncs. Until a caller populates it, revoke fails closed instead of guessing; the capability table in the docs carries a footnote saying so.

Permissions

Advertised permissions come from the x-permission block Datadog publishes for each operation in its own OpenAPI spec, not from inference:

Capability Endpoint Permission
Sync organization API keys ListAPIKeys api_keys_read
Revoke organization API keys DeleteAPIKey api_keys_delete
Sync, issue and revoke application keys List / Create / DeleteServiceAccountApplicationKey service_account_write

api_keys_write is deliberately not advertised: Datadog scopes it to create and rename, which no advertised capability calls. service_account_write sits only on the secret resource types, which are registered only when secret sync is on, so a secrets-off install is not asked to grant it.

Verification

  • go build ./cmd/baton-datadog — clean
  • go test ./... -count=1 — pass
  • make lint0 issues.
  • baton_capabilities.json regenerated from the built binary rather than hand-edited.

Unit coverage for the new paths: application-key create and delete plus provider 404 mapping, create with no returned key material, FindAPIKeyByName exact-match-after-filter including a cross-page match, duplicate-request refusal, handle-is-not-the-secret assertions on both delete paths, one-provider-page-per-List-call pagination, the permission-denied skip, and warning sampling.

Live provider

The live smoke test is opt-in behind DATADOG_CREDENTIAL_SMOKE=1 and is skipped in CI. It was run against a disposable Datadog trial organization at 0228d6eb and passed: the key was minted, found through ListServiceAccountApplicationKeys, used to authenticate a real request, revoked, confirmed to stop authenticating, and confirmed delisted from its service account.

Ownership was confirmed independently of the connector, which is the claim the retarget rests on:

  • the org application-key record reports owned_by as the named service account, and that owner's user record has service_account: true;
  • the key returns 404 under a different service account and 404 as a current-user application key;
  • the key is absent from the organization API-key list and from the current-user application-key list.

Delete scoping was exercised live as well: revocation goes through the service-account-scoped endpoint.

The two commits since 0228d6eb are test-only, so this evidence still describes the current code.

Not covered by that run, and stated so this is not read as broader than it is: multi-page application-key pagination was not exercised, and neither was reduced-permission behavior — the trial credential holds Datadog Admin-role permissions, so the run shows that service_account_write is sufficient but not that it is necessary.

Notes

  • Datadog application keys do not support create-time expiry.
  • Datadog may retain key metadata after revocation, so authentication failure is the revoke assertion — and only a 401/403 counts as evidence, not any error.
  • Syncing every Datadog secret, and allowing delete of any secret including keys this connector never created, is intended.
  • Deleting a key that is already absent is treated as success. A revoke that names the wrong owning service account is currently indistinguishable from that case and also returns success while the key survives; this is judged unreachable in the current caller flow and is tracked separately rather than changed here.
  • A List call returns at most one provider page so the SDK keeps control of checkpointing, rate limits and cancellation; a service account whose keys the role cannot read is warned about and skipped rather than failing the whole sync.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Comment thread pkg/client/client.go Outdated
Comment thread pkg/connector/users.go
Comment thread pkg/connector/users.go Outdated
Comment thread pkg/connector/api_token.go Outdated
Comment thread pkg/connector/users.go
@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Connector PR Review: Add Datadog credential issuance

Blocking Issues: 0 | Suggestions: 3 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 3aeafaf12b6b.
Review mode: full
View review run

Review Summary

Scanned the full PR diff for security and correctness: the new applicationKeyBuilder two-level pagination walk (users → per-service-account application keys) is correct against the SDK's pagination.Bag stack semantics and genuinely returns one provider page per List call; Issue's service-account re-check, duplicate refusal, and mint-then-rollback path are sound, and the SDK rejects an expiry request before Issue runs so the no-expires_at mapping cannot orphan a key. The prior finding on credential_lifecycle_test.go:984 is addressed — currentKeys is now guarded by keysMu and read through getKeys(). go.mod only promotes stretchr/testify from indirect to direct, matching the new require-based tests, and baton_capabilities.json / docs/connector.mdx are consistent with the registered resource types and the advertised service_account_write / api_keys_delete permissions.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • pkg/client/client_test.go:159body is shared between the httptest handler goroutine and the test body without synchronization; the same shape was just guarded with bodyMu/keysMu in credential_lifecycle_test.go.
  • pkg/connector/application_key.go:177ListUsers omits WithPageSize, so the users walk runs at Datadog's default 10/page (10× more round-trips than the defaultV2PageSize that apiTokenBuilder.List now requests).
  • pkg/connector/application_key.go:374 — an unparseable created_at aborts the entire sync, and this new path has no test coverage.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/client/client_test.go`:
- Around line 159-171 (subtest "create sends requested scopes"): the local `var body string`
  is assigned inside the httptest handler (line 162, on the server goroutine) and read by
  the test body at lines 170-171. net/http provides no happens-before edge between the
  handler returning and the client's request completing, so this is an unsynchronized
  shared write/read. Fix it the same way the last commit fixed
  pkg/connector/credential_lifecycle_test.go: add a `sync.Mutex` plus `setBody`/`getBody`
  closures (mirroring `bodyMu`/`setCreateBody`/`getCreateBody` there) and read the captured
  body through the getter before asserting on it.

In `pkg/connector/application_key.go`:
- Around line 177: `o.wrapper.ListUsers(ctx, datadogV2.NewListUsersOptionalParameters().WithPageNumber(page))`
  does not set a page size, so Datadog applies its default `page[size]` of 10. Since
  `listServiceAccountsPage` consumes exactly one users page per `List` call, this makes the
  application-key sync issue roughly 10x more users requests than necessary. Add
  `.WithPageSize(defaultV2PageSize)` to match what `apiTokenBuilder.List` now does. The
  `len(data) == 0` termination check stays correct unchanged.
- Around line 374-380: when `attrs.CreatedAt` fails to parse as RFC3339Nano,
  `applicationKeyResource` returns an error and `listApplicationKeyPage` propagates it, so
  one malformed timestamp on one key aborts the entire sync including service accounts not
  yet walked. This path is also untested: no `TestApplicationKeyResourceScopesProfile`
  fixture sets `created_at`, and the live smoke test never calls `applicationKeyResource`.
  Either add a `created_at` case (valid and malformed) to that table test, or log at
  Debug/Warn and omit `WithResourceCreatedAt` instead of failing, so an unexpected provider
  timestamp format degrades one resource rather than the whole sync.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking issues found — see review comments.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Comment thread pkg/client/client.go Outdated
Comment thread baton_capabilities.json
Comment thread docs/connector.mdx Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking issues found — see review comments.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Comment thread pkg/connector/credential_smoke_test.go
Comment thread pkg/connector/credential_smoke_test.go Outdated
Comment thread pkg/connector/credential_smoke_test.go Outdated
Comment thread pkg/connector/credential_smoke_test.go Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking issues found — see review comments.

santhosh-c1 and others added 6 commits August 20, 2026 06:57
Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Comment thread pkg/client/client.go Outdated
Comment thread pkg/client/client.go Outdated
Comment thread pkg/connector/credential_smoke_test.go Outdated
Comment thread pkg/connector/connector.go
Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Fixes formatting on api_token.go (tab indent) and resource_types.go (trailing newline).

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Comment thread pkg/connector/application_key.go Outdated
Comment thread pkg/connector/application_key.go Outdated
Comment on lines +79 to +85
if parentResourceID == nil || parentResourceID.GetResourceType() != userResourceType.Id {
return nil, status.Error(codes.InvalidArgument, "baton-datadog: the owning service account id is required to delete a service account application key")
}
serviceAccountID := parentResourceID.GetResource()
if isMalformedAPIKeyHandle(serviceAccountID) {
return nil, status.Errorf(codes.InvalidArgument, "baton-datadog: owning service account id %q is malformed", serviceAccountID)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Delete hard-fails with InvalidArgument whenever parentResourceID is absent, and the doc comment above states that no C1 caller populates it today. Meanwhile baton_capabilities.json advertises CAPABILITY_RESOURCE_DELETE for this type (the SDK requires a deleter for any credential-issue secret type) and docs/connector.mdx:151 shows Revoke ✓ for service account application keys — so an issued key that C1 believes is revocable may in practice never be revocable. A fallback would close this without depending on a platform change: when parentResourceID is missing, resolve the owner via the org-level GET /api/v2/application_keys/{app_key_id} (relationships.owned_by) before deleting. If you'd rather keep the fail-closed behavior, the docs capability table should say revoke is not yet wired for this type rather than ✓.

Comment thread pkg/connector/credential_smoke_test.go Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

…er call

The advertised Datadog permissions did not match what the endpoints this
connector actually calls require, per the per-operation "x-permission" blocks
in Datadog's own API spec and the published role-permission table:

- DeleteAPIKey requires api_keys_delete. api_keys_write covers only
  CreateAPIKey/UpdateAPIKey ("Create and rename API Keys"), which no
  advertised capability on the api-key type calls, so it is dropped rather
  than making operators grant org-wide key-creation rights the connector
  never exercises.
- Every service-account application-key endpoint the connector calls --
  list (sync), create (issue) and delete (revoke) -- requires
  service_account_write. user_access_manage covers user disable, role
  management, SAML-to-role mappings and logs restriction queries, and grants
  none of the three, so sync/issue/revoke were advertised against a role
  Datadog answers with a 403. service_account_write is added to the
  service-account-application-key type and to the user type, where
  CAPABILITY_CREDENTIAL_ISSUE is registered.

baton_capabilities.json is regenerated from the built binary rather than
hand-edited, and the docs' custom-role guidance now names the same set.

applicationKeyBuilder.List drained every application-key page for every
service account inside a single call, buffering the whole org's keys before
the SDK could checkpoint, respect rate limits or cancel. It now returns one
provider page per call, keeping the users page and one child state per
discovered service account in the pagination bag, and a 403 or 404 for a
single service account is warned and skipped instead of failing the whole
sync. Both behaviours are covered by new tests.

Also drop GetAPIKey and ValidateAPIKey, which no production code and no test
referenced after the smoke-test rework, and make the smoke test's
canAuthenticate probe separate a credential rejection from a transient
failure so the revocation assertion cannot pass on a 429 or a 5xx.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Comment thread pkg/connector/resource_types.go Outdated
Comment thread pkg/connector/resource_types.go
Comment thread pkg/connector/application_key.go Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@highb
highb dismissed github-actions[bot]’s stale review August 22, 2026 03:45

judge is not clearing reviews

…p warning

Three follow-ups on the permission metadata, all in the surfaces the previous
commit touched.

Drop service_account_write from userResourceType. That type is registered on
every install, but issuance only exists when sync-secrets is on -- the same
flag that swaps credentialUserBuilder in for userBuilder and registers
applicationKeyBuilder at all -- so a sync-secrets-off install was being told
to grant a Datadog Admin permission no code path in that configuration can
reach. baton_capabilities.json cannot express the condition directly: it is
one static document generated from a connector built with SyncSecrets and
SyncSchedules forced true (cmd/baton-datadog/main.go), and
CapabilityPermissions has no conditional form. Scoping the permission to
serviceAccountApplicationKeyResourceType, which is only registered under the
flag, is how the conditionality is carried; the user type's credential_issue
block already points at that type via secretResourceTypeId, so the
requirement stays discoverable. This also makes the metadata agree with what
docs/connector.mdx already said.

Confirm service_account_write as the permission for the service-account
application-key endpoints, and do not also advertise user_access_manage.
Datadog's published OpenAPI spec gives each operation an "x-permission" OR
list, and for ListServiceAccountApplicationKeys,
CreateServiceAccountApplicationKey and DeleteServiceAccountApplicationKey
that list has exactly one entry. The same spec does name several accepted
permissions where several are accepted -- DisableUser and UpdateUser accept
user_access_manage OR service_account_write -- so the single-entry list is
meaningful rather than an omission. user_access_manage is an AuthZ oauth2
scope, a different axis from RBAC that does not apply to a connector
authenticating with apiKeyAuth and appKeyAuth. Recorded in the type's doc
comment so the next reader does not have to re-derive it.

Sample the per-service-account skip warning. The case it exists for is a role
missing service_account_write org-wide, which made it fire once per service
account on every sync. It now logs the 1st, 10th and 100th occurrence and
every 1000th after that, with a total_occurrences field, per criteria L7.
Neither this repo nor the vendored baton-sdk ships a sampling helper, so
shouldLogSampled is the smallest thing that satisfies the criteria: a pure
function of the count, with the counter owned by the builder.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Comment thread pkg/connector/application_key.go

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

applicationKeyBuilder is constructed once per connector process --
Datadog.ResourceSyncers runs inside connectorbuilder.NewConnector, which the
SDK calls once at startup and whose registered syncers are reused for every
sync -- so skippedServiceAccounts never reset. The first sync consumed the
1st, 10th and 100th log slots, and a second sync against the same org-wide
missing service_account_write incremented the total while emitting nothing at
all until it reached 1000. That is worse than the per-resource noise the
sampling was added to prevent.

List now zeroes the counter when the page token is empty, giving each walk its
own schedule. An empty token is a safe first-walk signal here: List only
returns an empty NextPageToken from bag.Marshal() with an empty bag, and the
bag can only be emptied by popping the users-level state, which happens solely
on an empty users page -- the end of the walk -- so no mid-walk call can carry
one. A retried first page resets again, which is what a restarted walk wants.

TestApplicationKeyBuilderListSamplesSkipWarning now drains twice against the
same builder and asserts each walk logs its own occurrence 1 and 10. Without
the reset the second walk logs nothing, so the assertion discriminates. The
drain helper also asserts no mid-walk token is empty, pinning the invariant the
reset relies on.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Comment thread pkg/connector/credential_lifecycle_test.go Outdated
Comment thread pkg/connector/credential_lifecycle_test.go Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

Two assertions added with the sampling tests could not fail, which is worse
than no assertion because it reads as coverage.

require.NotEmpty on results.NextPageToken sat three lines below an
`if results.NextPageToken == "" { break }`, so the only input that could have
failed it had already left the loop. The invariant it claimed to pin -- that an
empty token means first-call-of-this-walk, which is what List's counter reset
depends on -- was asserted nowhere. The check now lives in the break branch and
asserts the walk did not terminate on call zero, which is the property that
makes an empty token unambiguous between start and end. Early termination
partway through a walk is caught separately by the attempted-request count.

The occurrence assertion compared the log line against the substring
`"total_occurrences":1`, which is a prefix of :10, :100 and :1000, so a walk
whose first sampled line was a later occurrence would still have passed -- and
that is precisely the property the per-walk reset exists to prove. Skip warnings
are now decoded from the JSON records and total_occurrences is compared
numerically, so 1 cannot be satisfied by 10. Decoding also tightened the rest:
the message is matched exactly instead of by substring, total_occurrences must
be present rather than merely mentioned, the gRPC code must be PermissionDenied,
and the record must name a service account.

Every one of these was checked by removing the behaviour it describes and
confirming the test fails: an empty users page, a sampling schedule that skips
occurrence 1, a 404 in place of the 403, each log field dropped in turn, a
reworded message, a single walk instead of two, and the counter reset removed.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

SecretTrait has no scopes field, so a synced application key gave C1 no way to
see what the credential is actually allowed to do. Resource.Profile is free-form
and has a first-class setter, so the scopes ride there, on both the sync path
and the issuance path -- a freshly vended key reports its scopes immediately
rather than only after the next sync rebuilds it.

Datadog's scopes field is a three-state nullable list: absent, explicit null, or
a list. The profile collapses that to two states, chosen so no state is
misreported:

  - Datadog did not report scopes: the key is absent from the profile. Emitting
    an empty list would assert the key is unscoped, which the response never
    said, and that is the one error a consumer cannot detect.
  - Datadog reported an unscoped key, as explicit null or an empty list: the key
    is present and empty, positively stating that the key carries its owner's
    full permissions.
  - Datadog reported scopes: the key holds them.

The value is therefore always a list when present and never null, so a consumer
never needs a type switch, and absence is reserved for the one distinction that
matters. The field is named "scopes" rather than a provider-prefixed name: a
consumer should be able to learn what a synced credential can do without
knowing which provider minted it, and this repo's existing profiles are
unprefixed too.

Issuance records what the provider echoed back rather than what was requested,
since Datadog may normalize the list, so an issued key agrees with what the
syncer will later report for it instead of drifting from it. That required the
client to carry the created key's scopes, which it previously discarded.

The scopes pass-through into CreateServiceAccountApplicationKey had never been
exercised by any test. It is now, along with a scoped key, an unscoped key, a
key whose scopes change between syncs, a provider that reports nothing, and the
guarantee that setting a resource profile does not disturb the secret trait.
Each new assertion was checked by removing the behaviour it describes and
confirming the test fails.

baton_capabilities.json is unchanged: profile data is per-resource sync output,
not capability metadata. Verified by regenerating and diffing.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Comment thread pkg/connector/credential_lifecycle_test.go Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

A fake provider's handler runs on the httptest server's goroutine while the test
body reads and writes the same variables. net/http promises no happens-before
edge between the two, so three sites in the tests added for this PR relied on
runtime implementation detail rather than on the memory model:

- TestApplicationKeyListCarriesScopes rewrote the upstream key set between two
  drains while the handler was reading it.
- TestIssuePassesScopesToProviderAndProfile captured the create request body in
  the handler and read it from the test body.
- recordRequest appended to a shared slice from the handler, which every test
  using a recording fake then iterated. Guarding the helper is the minimal fix
  for the call sites this PR added, and it covers the pre-existing ones in the
  same file as a side effect.

Each is now behind a mutex, with accessors at both ends. The rescope test keeps a
single server across both drains on purpose: giving each drain its own server
would remove the race by no longer exercising the rescope-mid-life path the test
exists for. All three guards were checked by neutering them and confirming the
assertions they protect fail.

Worth recording for anyone auditing this later: `go test -race` does NOT report
any of these, at this commit or in a minimal isolated reproduction run twenty
times. The socket path between an httptest client and its handler appears to
carry enough runtime synchronization for the detector to see an edge. So this is
a robustness fix argued from the memory model, not a detector-confirmed failure,
and -race is added to the gates because it is cheap and catches other classes --
not because it would have caught this.

Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com>
Comment thread pkg/client/client_test.go
Comment on lines +159 to +171
var body string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
buf, _ := io.ReadAll(r.Body)
body = string(buf)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":{"id":"appkey-id","type":"application_keys","attributes":{"key":"plaintext-app-key","name":"c1-request"}}}`))
}))
defer server.Close()

_, err := newOfficialTestClient(server.URL).CreateServiceAccountApplicationKey(context.Background(), serviceAccountID, "c1-request", []string{"dashboards_read", "metrics_read"})
assertNoError(t, err, "create service account application key should succeed")
assertContains(t, body, "dashboards_read", "request body should include the requested scopes")
assertContains(t, body, "metrics_read", "request body should include the requested scopes")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: body is written on the httptest handler goroutine (line 162) and read by the test body (lines 170-171) with no synchronization. This is exactly the shape the last commit guarded with bodyMu/keysMu in credential_lifecycle_test.gonet/http gives no happens-before edge between a handler returning and the client's Do returning, so the same reasoning applies here. Wrapping the write/read in a mutex (or using sync/atomic.Pointer) would make this file consistent with the fix already applied in the connector tests.

bag *pagination.Bag,
page int64,
) ([]*v2.Resource, *resource.SyncOpResults, error) {
users, err := o.wrapper.ListUsers(ctx, datadogV2.NewListUsersOptionalParameters().WithPageNumber(page))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: this ListUsers call sets only WithPageNumber, so Datadog applies its default page[size] of 10 (the vendored client's own paginator hardcodes pageSize_ := int64(10) for ListUsers). Since each List call consumes exactly one users page, an org with N users costs N/10 provider round-trips before any application key is fetched. apiTokenBuilder.List gained .WithPageSize(defaultV2PageSize) in this same PR; adding it here too would cut that by 10× without changing the len(data) == 0 termination condition.

Comment on lines +374 to +380
if attrs != nil && attrs.CreatedAt != nil {
createdAt, err := time.Parse(time.RFC3339Nano, *attrs.CreatedAt)
if err != nil {
return nil, fmt.Errorf("baton-datadog: parse application key created_at: %w", err)
}
resourceOptions = append(resourceOptions, resource.WithResourceCreatedAt(createdAt))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: a created_at value that does not parse as RFC3339Nano makes applicationKeyResource return an error, which listApplicationKeyPage propagates as a hard failure — one malformed timestamp on one key aborts the whole sync, including every service account not yet walked. That matches the existing apiTokenBuilder behaviour, but the app-key path is new and untested here: none of the TestApplicationKeyResource* fixtures include created_at, and the live smoke test never exercises applicationKeyResource (it calls ListServiceAccountApplicationKeys directly). Worth either adding a created_at case to the table test, or degrading to a Debug/Warn + omit-the-timestamp so an unexpected provider format cannot take down a whole sync.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants