Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions e2e/playground.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,33 @@ test.describe('Playground', () => {
await expect(page.locator('select[data-field-key="notediscovery.defaultTheme"]')).toHaveValue('light');
});

test('mongodb arbiter output stays within supported topology combinations', async ({ page }) => {
await page.goto('/playground');
await page.locator('.playground-chart-btn[data-slug="mongodb"]').click();

const architecture = page.locator('select[data-field-key="architecture"]');
const members = page.locator('input[data-field-key="replicaSet.members"]');
const arbiter = page.locator('button[data-field-key="arbiter.enabled"]');
const code = page.locator('#playground-code');

await arbiter.click();
await expect(architecture).toHaveValue('replicaset');
await expect(members).toHaveValue('2');
await expect(code).toContainText('architecture=replicaset');
await expect(code).toContainText('replicaSet.members=2');
await expect(code).toContainText('arbiter.enabled=true');

await members.fill('3');
await expect(code).not.toContainText('arbiter.enabled');

await arbiter.click();
await architecture.selectOption('standalone');
await expect(code).not.toContainText('arbiter.enabled');

await page.goto('/playground?chart=mongodb&architecture=replicaset&replicaSet.members=3&arbiter.enabled=true');
await expect(page.locator('#playground-code')).not.toContainText('arbiter.enabled');
});

test('ntfy defaults stay aligned with the chart', async ({ page }) => {
await page.goto('/playground');
await page.locator('.playground-chart-btn[data-slug="ntfy"]').click();
Expand Down
7 changes: 7 additions & 0 deletions src/data/playground-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4928,6 +4928,13 @@ export const chartConfigs: Record<string, ChartConfig> = {
default: '3',
description: 'Data-bearing members in replica set mode',
},
{
label: 'Arbiter',
key: 'arbiter.enabled',
type: 'toggle',
default: 'false',
description: 'Add one voting arbiter; requires 2, 4, or 6 data-bearing members',
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
label: 'Storage Size',
key: 'persistence.size',
Expand Down
28 changes: 19 additions & 9 deletions src/pages/docs/charts/mongodb.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,10 @@ persistence:

<Callout type="info" title="When to use an arbiter">
An arbiter participates in elections but holds no data. Use it when you have an even number of data-bearing members to
ensure a majority for elections. With 3 data members, an arbiter is not needed.
ensure a majority for elections. MongoDB recommends three data-bearing members when capacity permits because an
arbiter does not add data redundancy and can affect majority write availability. The chart supports one arbiter with
2, 4, or 6 data-bearing members. See the [official arbiter
considerations](https://www.mongodb.com/docs/manual/core/replica-set-arbiter/).
</Callout>

</div>
Expand Down Expand Up @@ -241,12 +244,13 @@ config:

### Architecture

| Parameter | Type | Default | Description |
| -------------------- | ------- | ------------ | -------------------------------------------------------- |
| `architecture` | string | `standalone` | `standalone`, `replicaset`, or `sharded`. |
| `replicaSet.name` | string | `rs0` | Replica set name. |
| `replicaSet.members` | integer | `3` | Number of data-bearing replica set members. |
| `arbiter.enabled` | boolean | `false` | Add an arbiter pod (votes in elections, stores no data). |
| Parameter | Type | Default | Description |
| -------------------- | ------- | ------------ | ---------------------------------------------------------------- |
| `architecture` | string | `standalone` | `standalone`, `replicaset`, or `sharded`. |
| `replicaSet.name` | string | `rs0` | Replica set name. |
| `replicaSet.members` | integer | `3` | Number of data-bearing replica set members. |
| `arbiter.enabled` | boolean | `false` | Add one non-data-bearing voting member in replica set mode. |
| `arbiter.resources` | object | `{}` | CPU and memory requests/limits for the arbiter mongod container. |

### Sharded Cluster

Expand Down Expand Up @@ -340,8 +344,14 @@ MongoDB administrative commands instead of changing chart values.
2. Deploy the chart in `replicaset` mode.
3. Restore data with `mongorestore`.

Changing `replicaSet.members` on an existing replica set triggers a reconfiguration.
Scaling down removes members — ensure no data is exclusively on the removed members.
Enabling or disabling `arbiter.enabled` reconciles the arbiter through the post-upgrade hook. Increasing
`replicaSet.members` adds missing members. Reducing it does not remove data-bearing members from MongoDB; follow MongoDB's
member-removal procedure before scaling down the StatefulSet.

When arbiter reconciliation would change MongoDB's implicit default write concern, the hook preserves the currently
effective value (`w: 1` or `majority`) by promoting it to a global default. Existing operator-managed global defaults are
left unchanged. MongoDB 5.0 and newer do not allow a global default write concern to be unset after it is established. See
the official [`setDefaultRWConcern` documentation](https://www.mongodb.com/docs/manual/reference/command/setDefaultRWConcern/).

</Callout>

Expand Down
28 changes: 27 additions & 1 deletion src/scripts/playground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const coupledRuleKeysBySlug: Record<string, string[][]> = {
};

const giteaPostgresqlPassword = 'change-me-gitea-postgresql';
const mongodbArbiterMemberCounts = new Set(['2', '4', '6']);

function getGroups(slug: string): GroupConfig[] {
return configs[slug] ?? configs['_default'] ?? [];
Expand Down Expand Up @@ -232,7 +233,7 @@ function autoEnableField(key: string) {
function setControlValue(key: string, value: string) {
currentValues[key] = value;
const control = controlsEl?.querySelector<HTMLInputElement | HTMLSelectElement | HTMLButtonElement>(
`[data-field-key="${key}"]`,
`input[data-field-key="${key}"], select[data-field-key="${key}"], button[data-field-key="${key}"]`,
);

if (!control) return;
Expand All @@ -259,6 +260,22 @@ function clearGiteaExternalDatabaseDetectionFields() {
}

function applyFieldSideEffects(key: string, value: string) {
if (selectedSlug === 'mongodb') {
if (key === 'arbiter.enabled' && value === 'true') {
setControlValue('architecture', 'replicaset');
if (!mongodbArbiterMemberCounts.has(currentValues['replicaSet.members'])) {
setControlValue('replicaSet.members', '2');
}
} else if (
currentValues['arbiter.enabled'] === 'true' &&
((key === 'architecture' && value !== 'replicaset') ||
(key === 'replicaSet.members' && !mongodbArbiterMemberCounts.has(value)))
) {
updateToggleField('arbiter.enabled', false);
}
return;
}

if (selectedSlug !== 'gitea') return;

if (key === 'database.mode' && value === 'postgresql') {
Expand Down Expand Up @@ -607,6 +624,7 @@ function buildFieldControl(field: FieldConfig): HTMLElement {
input.addEventListener('input', () => {
setFieldValue(field.key, input.value);
if (field.enables) autoEnableField(field.enables);
applyFieldSideEffects(field.key, input.value);
updateOutput();
});
controlDiv.appendChild(input);
Expand Down Expand Up @@ -997,6 +1015,14 @@ function setTreeValue(tree: Record<string, any>, key: string, value: boolean | n

function updateOutput() {
if (!codeEl || !selectedSlug) return;
if (
selectedSlug === 'mongodb' &&
currentValues['arbiter.enabled'] === 'true' &&
(currentValues['architecture'] !== 'replicaset' ||
!mongodbArbiterMemberCounts.has(currentValues['replicaSet.members']))
) {
updateToggleField('arbiter.enabled', false);
}
if (copyBtn) copyBtn.disabled = false;
if (shareBtn) shareBtn.disabled = false;

Expand Down