Skip to content

feat(accounts): the object-storage datasource provider is resolved from the composed service, not compiled into the registry - #1269

Draft
antoinetoussaint-byte wants to merge 1 commit into
issue-1211-object-storage-through-the-servicefrom
issue-1268-datasources-a-provider-is-resolved-from-a-composed
Draft

antoinetoussaint-byte wants to merge 1 commit into
issue-1211-object-storage-through-the-servicefrom
issue-1268-datasources-a-provider-is-resolved-from-a-composed

Conversation

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor

Closes #1268.

Stacked on #1235, draft until it merges. The base is #1235's branch, so the diff shows only this change. Once #1235 merges, this gets rebased onto main, retargeted, and its checks counted on the SHA that will merge. CI does not run on a stacked PR (ci.yml triggers on pull_request: branches: [main]), so for now the evidence is the local runs below.

What changes

  • The connector registry is built from the compiled-in providers only: github (conformant), api and crawler (non-conformant with their gap). Object storage is no longer one of them.
  • SetObjectStorage registers object storage, and only after objectstore.CheckService has confirmed that the composed service can serve a per-call binding with a batched read. work.go calls it once the declared object-storage dependency resolves. Registration uses Registry.Register's own validation and wraps the connector in connector.Budgeted.
  • When the service isn't composed, or can't read a bucket:
    • the catalog has no upload entry;
    • AddSource(upload) fails with FailedPrecondition and ErrObjectStorageUnavailable, i.e. by name, not as an unknown provider;
    • an existing source's sync fails with the same error;
    • DatasourceConformance gives that same sentence as the source's gap.
  • Everything the host does for the provider now lives in pkg/business/datasource_objectstore.go. That covers the setters, registration, per-call bucket resolution, retained listings, runUploadSync and the park step; the code is moved, not rewritten. datasources.go and datasource_connectors.go no longer import objectstore. The sync now sizes its batches from the registered descriptor's Budget (MaxItemsPerCall / MaxBytesPerCall) rather than from package constants. The values are the same.
  • github, api and crawler are unchanged. The four provider constants stay.

Envelope

  • Clause 3 (credentials). The credential is still decrypted from the source on every call and placed on that call's binding only. The provider service never returns or stores it.
  • Per-credential budget. It stays host-side. datasourceScheduler over datasource_credential_budgets meters across every source and replica, and the composed service never sees it.

Reused, and deliberately not

Reused: Registry.Register and its descriptor validation, Descriptor, connector.Budgeted with the host's datasourceScheduler, connectortest.RunFiles, #1235's objectstore.CheckService / ServiceBucket client, and the existing dialObjectStorage resolution of the declared dependency.

Deliberately not built: a provider resolver, a table of composed providers, a manifest field or any other second mechanism. With one composed provider, the seam is "the dependency resolves, so Register". A generic list would only be justified by a published provider contract, which is step 1 after this issue.

Fix or hack

Fix, at the owner: the host's build no longer decides whether object storage exists. Composition does. No stopgap.

Docs (same PR)

  • module/services/accounts/AGENTS.md: a new section, A datasource provider is compiled in, or resolved from a composed service. The object-storage admission bullet is rewritten to match.
  • module/services/accounts/code/pkg/datasource/connector/CONFORMANCE.md: object storage is registered through its service, the suite runs as registered, and registration needs a capable service.
  • module/services/README.md: the provider exists only where object-storage is composed. The topology itself is unchanged; feat(accounts): object-storage sources read through the composed object-storage service; the host's store client is deleted #1235 already composes the service. module/DEPLOYMENT_TOPOLOGY.md needs no change.

Tests

  • New: TestObjectStorageAsRegisteredPassesTheConformanceSuite. It runs connectortest.RunFiles against DatasourceConnectors().Files("upload") after the provider is resolved, on sources connected through AddSource. That exercises the credential the host decrypts per call and the listings retained in the host's store. 14 cases pass and case 05 is skipped, the same as the package-level run (readers are source-scoped).
  • Mutation check: I changed the secret the host hands to the bucket, and cases 02–09 failed. Then I restored it.
  • Rewritten: TestObjectStorageIsAdmittedOnlyWhereAServiceCanReadIt is now TestObjectStorageIsRegisteredOnlyWhereItsServiceResolves. It checks that the provider is absent from the catalog, that AddSource refuses it by name, that the conformance gap names it, and that the provider is present and admitted once resolved. The 0.0.18-service admission test now expects the provider to be absent rather than listed but refusing.
  • Local results:

Run first: the three behaviours, from codefly run

I used codefly v0.1.215, the CI pin (darwin-arm64 archive, digest checked against the release's checksums.txt). The local development CLI fails at accounts runtime-init with no configuration found for egress. I logged in through the gateway with the dev-admin fixture (/v1/auth/authenticate) and called Connect on the gateway's REST port.

Composed: codefly run service saas-starter/auth-gateway --fixture dev-admin --headless --temporary-ports → Will run 6 service(s): … saas-starter/object-storage@0.0.0 via codefly.dev/object-storage:0.0.19 …

GetDatasourceCatalog:
api conformant= False accepts_new_sources= False
crawler conformant= False accepts_new_sources= False
github conformant= True accepts_new_sources= True
upload conformant= True accepts_new_sources= True
AddSource(upload) → {'provider': 'DATASOURCE_PROVIDER_UPLOAD', 'status': 'DATASOURCE_STATUS_ACTIVE', 'conformant': True}

Not composed: the same command with --exclude-dependency saas-starter/object-storage → Will run 5 service(s) (no object-storage). Accounts logs the composed object-storage service cannot read a tenant's bucket; object-storage datasources are unavailable error=… dial tcp 127.0.0.1:52123: connect: connection refused.

GetDatasourceCatalog:
api conformant= False accepts_new_sources= False
crawler conformant= False accepts_new_sources= False
github conformant= True accepts_new_sources= True
AddSource(upload) → HTTP 400 {"code":"failed_precondition","message":"object storage is not available here: no composed object-storage service serves a per-call binding with a batched read"}

Not verified

  • codefly ci run (the canonical service gate) was not run locally. CI will run it after the rebase onto main.
  • An existing object-storage source in a composition without the service was checked by unit test only (DatasourceConformance, and the sync's ErrObjectStorageUnavailable). It was not exercised at runtime: each --temporary-ports run gets its own state, so the source created in the composed run was not visible to the excluded run.
  • Address for an excluded dependency: with --exclude-dependency saas-starter/object-storage, accounts still resolved an endpoint address for the excluded service, and the dial was refused. The host's behaviour is correct either way: no address, or a service that cannot answer, both leave the provider unregistered. But the absent run went through the "cannot read" branch, not the "no object-storage service is composed" branch. Whether the CLI should export an endpoint for an excluded dependency is a question for codefly-dev/cli. I have not filed it.

🤖 Generated with Claude Code

…om the composed service, not compiled into the registry

The connector registry is built with the compiled-in providers only (github,
api, crawler). Object storage is registered through Registry.Register,
wrapped in connector.Budgeted, when the declared object-storage dependency
resolves to a service that can read a bucket (SetObjectStorage). A
composition without that service has no upload entry in the catalog, and
AddSource and sync refuse a source of it by name.

Everything the host does for the provider moves to
datasource_objectstore.go; datasources.go no longer imports objectstore,
and the sync batches by the registered descriptor's budget. The
conformance suite also runs against the connector the registry holds.

Closes #1268

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant