-
Notifications
You must be signed in to change notification settings - Fork 72
feat: role managed RGW placement #814
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
dfa3b0a
6dc0ca3
506af25
9c434da
b9493a2
bf81e5e
e352d47
b8443fa
7cd1915
be69165
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| backend | ||
| backends | ||
| bitmask | ||
| boolean | ||
| Charmcraft | ||
| cjk | ||
| cryptographically | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| .. meta:: | ||
| :description: Reference information for the MicroCeph declarative placement API, including the policy schema, capability markers, and the supported ownership model. | ||
|
|
||
| .. _declarative-placement: | ||
|
|
||
| Declarative placement | ||
| ===================== | ||
|
|
||
| Declarative placement lets an external orchestrator, such as the MicroCeph | ||
| charm, describe which cluster members should run which services. The | ||
| orchestrator submits a desired-state policy and MicroCeph reconciles the | ||
| cluster to match it, owning all destructive-operation safety itself. | ||
|
|
||
| Placement is an API-only surface. There is no ``microceph`` subcommand for it; | ||
| it is intended to be driven by an orchestrator rather than by hand. | ||
|
|
||
| Capability markers | ||
| ------------------ | ||
|
|
||
| Before using placement, a consumer should confirm the running snap revision | ||
| supports it by querying ``GET /1.0/cluster/capabilities``. The markers relevant | ||
| to placement are: | ||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
|
|
||
| * - Marker | ||
| - Meaning | ||
| * - ``declarative-placement`` | ||
| - The ``/1.0/placement`` endpoints exist and control services (MON, MGR, | ||
| MDS) are reconciled. | ||
| * - ``placement-rgw`` | ||
| - The ``rgw`` field accepts an object carrying frontend configuration, and | ||
| ``GET`` reports an observed ``rgw_frontend``. | ||
|
|
||
| A consumer that needs RGW placement must gate on ``placement-rgw``. A snap | ||
| revision without it rejects the object-shaped ``rgw`` field with HTTP 400. | ||
|
|
||
| Endpoints | ||
| --------- | ||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
|
|
||
| * - Method | ||
| - Path | ||
| - Effect | ||
| * - ``PUT`` | ||
| - ``/1.0/placement`` | ||
| - Apply a policy, then store it as the declared intent. | ||
| * - ``GET`` | ||
| - ``/1.0/placement`` | ||
| - Return the declared policy, the observed placement, and lifecycle state. | ||
| * - ``DELETE`` | ||
| - ``/1.0/placement`` | ||
| - Clear the declared policy. Services are not added or removed. | ||
|
|
||
| Policy schema | ||
| ------------- | ||
|
|
||
| The ``PUT`` body is a policy document. ``mode`` is required and must be | ||
| ``reconcile``; an omitted or unknown mode is rejected, so a policy written for | ||
| a future mode fails loudly against an older snap rather than being silently | ||
| applied. | ||
|
|
||
| .. code-block:: json | ||
|
|
||
| { | ||
| "mode": "reconcile", | ||
| "members": { | ||
| "node-a": { "control": true }, | ||
| "node-b": { "control": false }, | ||
| "node-c": { "rgw": { "enabled": true, "port": 80 } } | ||
| } | ||
| } | ||
|
|
||
| Members absent from ``members`` are never touched. For members that are | ||
| present, an omitted field leaves that service untouched on that member, which | ||
| is distinct from an explicit ``false`` requesting removal. | ||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
|
|
||
| * - Field | ||
| - Type | ||
| - Effect | ||
| * - ``control`` | ||
| - boolean | ||
| - ``true`` places MON, MGR and MDS on the member; ``false`` removes them, | ||
| subject to the keep-one invariant below. | ||
| * - ``rgw`` | ||
| - object | ||
| - ``{"enabled": true}`` places RGW; ``{"enabled": false}`` removes it. See | ||
| :ref:`declarative-placement-rgw` below. | ||
| * - ``nfs`` | ||
| - array | ||
| - Accepted and reported, but not yet reconciled by the placement engine. | ||
| * - ``storage_eligible`` | ||
| - boolean | ||
| - Accepted and reported, but not yet enforced by the disk APIs. | ||
|
|
||
| Safety rules | ||
| ------------ | ||
|
|
||
| The engine applies additions before removals, so a migration brings the | ||
| replacement up before tearing the previous instance down. | ||
|
|
||
| Control services are further protected by a **keep-one invariant**: the engine | ||
| refuses to remove the last viable MON, MGR or MDS. Viability is checked against | ||
| Ceph itself (MON quorum, MGR active or standby, MDS up) rather than against | ||
| database records, so a service that exists but is not healthy is still a | ||
| removal target while never counting as the last retainer. | ||
|
|
||
| RGW has no keep-one invariant. It is a stateless gateway that may be scaled to | ||
| zero, so a policy that disables RGW on every member is honoured. | ||
|
|
||
| If a removal is refused, the requested additions remain in effect, the policy | ||
| is still stored as the declared intent, and the reason is reported in the | ||
| ``placement_refusal`` field of ``GET /1.0/placement``. | ||
|
|
||
| .. _declarative-placement-rgw: | ||
|
|
||
| RGW placement | ||
| ------------- | ||
|
|
||
| The ``rgw`` field carries both placement intent and beast frontend | ||
| configuration, so that presence and frontend settings are applied together. | ||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
|
|
||
| * - Field | ||
| - Type | ||
| - Notes | ||
| * - ``enabled`` | ||
| - boolean | ||
| - Whether the member should run RGW. | ||
| * - ``port`` | ||
| - integer | ||
| - Unencrypted listener port. Defaults to 80 when no TLS material is given. | ||
| * - ``ssl_port`` | ||
| - integer | ||
| - TLS listener port. Ignored unless both certificate and key are given. | ||
| * - ``ssl_certificate`` | ||
| - string | ||
| - base64-encoded PEM certificate. | ||
| * - ``ssl_private_key`` | ||
| - string | ||
| - base64-encoded PEM private key. | ||
|
|
||
| Re-applying the same configuration is a no-op: the member compares the desired | ||
| frontend against what is on disk and restarts RGW only when something actually | ||
| changed. Rotating a certificate is therefore an ordinary ``PUT`` with the new | ||
| material. | ||
|
|
||
| TLS key material is write-only. It travels over the authenticated API to the | ||
| member that needs it and is written to disk there, but it is stripped before | ||
| the policy is stored and is never returned by ``GET``. The observed | ||
| ``rgw_frontend`` reports ports and a TLS on/off flag only. A consumer that | ||
| manages TLS must therefore hold its own copy of the material; it cannot read it | ||
| back from MicroCeph. | ||
|
|
||
| Reading placement state | ||
| ----------------------- | ||
|
|
||
| ``GET /1.0/placement`` returns the declared policy alongside an ``observed`` | ||
| list describing what each member is actually running. Comparing the two is the | ||
| supported way to determine whether the cluster has converged. | ||
|
|
||
| Response codes | ||
| -------------- | ||
|
|
||
| .. list-table:: | ||
| :header-rows: 1 | ||
|
|
||
| * - Code | ||
| - Meaning | ||
| * - 400 | ||
| - The request cannot be satisfied: Ceph is not bootstrapped, the policy | ||
| names an unknown member, the RGW frontend configuration is malformed, or | ||
| a removal was refused by the keep-one invariant. | ||
| * - 409 | ||
| - Another placement apply, or a Ceph bootstrap, is in progress. The | ||
| request can be retried. | ||
|
|
||
| .. _declarative-placement-ownership: | ||
|
|
||
| Ownership model | ||
| --------------- | ||
|
|
||
| Placement applies are serialised cluster-wide, so concurrent policy | ||
| submissions cannot interleave. That serialisation covers the placement API | ||
| only. | ||
|
|
||
| .. important:: | ||
|
|
||
| Managing the same services through both the placement API and the | ||
| ``microceph enable`` / ``microceph disable`` commands is **not a supported | ||
| configuration**. Pick one owner for a given cluster. | ||
|
|
||
| The two mechanisms are independent write paths. The service commands do not | ||
| participate in the placement lock, do not consult the declared policy, and are | ||
| not subject to the keep-one invariant. Consequently, on a cluster driven by an | ||
| orchestrator: | ||
|
|
||
| - Enabling or disabling a service by hand puts the cluster out of step with the | ||
| declared policy. Nothing corrects this until the orchestrator submits its | ||
| next policy, at which point the manual change is reverted without warning. | ||
| - A service command issued while an apply is in flight can interleave with it. | ||
| Observed state may briefly disagree with what is running until the next | ||
| policy is applied. | ||
| - The keep-one invariant does not protect manual removals. ``microceph disable | ||
| mon`` will remove a MON that the placement engine would have refused to | ||
| remove. | ||
|
|
||
| Divergence is visible: ``GET /1.0/placement`` reports observed placement | ||
| alongside the declared policy, so the gap can be inspected at any time. | ||
|
|
||
| This restriction applies only to services that placement manages. On a cluster | ||
| that does not use placement, the service commands remain the normal way to | ||
| manage services and are fully supported. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -51,7 +51,8 @@ const placementPutTimeout = 10 * time.Minute | |
| func isClientSidePlacementError(err error) bool { | ||
| return errors.Is(err, ceph.ErrCephNotBootstrapped) || | ||
| errors.Is(err, ceph.ErrUnknownPlacementMember) || | ||
| errors.Is(err, ceph.ErrKeepOneInvariant) | ||
| errors.Is(err, ceph.ErrKeepOneInvariant) || | ||
| errors.Is(err, ceph.ErrRgwFrontendInvalid) | ||
| } | ||
|
|
||
| // inProgressResponse maps an "already in progress" sentinel (placement apply or | ||
|
|
@@ -147,7 +148,7 @@ func cmdPlacementPut(s mcTypes.State, r *http.Request) mcTypes.Response { | |
| // intent, and last_refusal records what failed so the caller can retry | ||
| // the same policy to converge. | ||
| if errors.Is(applyErr, ceph.ErrKeepOneInvariant) { | ||
| storeErr := ceph.StorePlacementPolicyFunc(ctx, interfaces.CephState{State: s}, policy) | ||
| storeErr := ceph.StorePlacementPolicyFunc(ctx, interfaces.CephState{State: s}, ceph.PolicyForStorage(policy)) | ||
| if storeErr != nil { | ||
| logger.Warnf("failed to store placement policy after keep-one refusal: %v", storeErr) | ||
| } | ||
|
|
@@ -176,8 +177,11 @@ func cmdPlacementPut(s mcTypes.State, r *http.Request) mcTypes.Response { | |
| logger.Warnf("failed to clear placement refusal: %v", clearErr) | ||
| } | ||
|
|
||
| // Persist the policy only after successful application. | ||
| err = ceph.StorePlacementPolicyFunc(ctx, interfaces.CephState{State: s}, policy) | ||
| // Persist the policy only after successful application. Strip RGW SSL key | ||
| // material before storage (CE142 Option B secrets posture): the cert/key | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: I think we could just remove the mention of option B. Correct me if I'm wrong but I don't think we ever actually mentioned what option b is. It's possible to tell through deduction what that decision was, but it might be a little confusing if someone tries to figure out what this means. This appears in various places across the PR |
||
| // travel over the authenticated API to the member that needs them, then are | ||
| // dropped so they are never persisted in dqlite. | ||
| err = ceph.StorePlacementPolicyFunc(ctx, interfaces.CephState{State: s}, ceph.PolicyForStorage(policy)) | ||
| if err != nil { | ||
| logger.Errorf("failed to store placement policy: %v", err) | ||
| return mcTypes.InternalError(err) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The documented 400 for malformed TLS is a 500 in practice.
ErrRgwFrontendInvalidis raised insideapplyRGWFrontend, which runs on the target member. It is flattened twice before the handler can test it:ceph/services_placement.go:112wraps with%v, not%w.client.SendServicePlacementReqthen crosses an HTTP boundary, which reconstructs a generic error regardless.So
errors.IsinisClientSidePlacementErrorcannot match, andSmartErrorfalls back to 500. The test covering this (api/placement_test.go:740) stubsApplyPlacementFuncand returns the wrapped sentinel directly, so it passes either way.Fixing the
%vand mappingErrRgwFrontendInvalidto 400 on the receiving member's handler would make the doc true; a test that goes through the real dispatch would keep it that way.