Add S3 erasure swarm with CloudFront signed magnets.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Operators configure bucket and CloudFront domain with env credentials; deploy plans upload RS 4+2 shards and attach signed edge URLs to BGP swarm magnets. Agents fetch LAN, CloudFront, then C2. Forge panel adds test and IAM policy JSON.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor, cleanup } from '@testing-library/react';
|
||||
import AwsErasureSwarmPanel from './AwsErasureSwarmPanel';
|
||||
|
||||
vi.mock('../../api/client', () => ({
|
||||
api: {
|
||||
updateConfig: vi.fn().mockResolvedValue({}),
|
||||
testErasureSwarm: vi.fn().mockResolvedValue({ ok: true }),
|
||||
getErasureSwarmPolicyJSON: vi.fn().mockResolvedValue({ iam_policy: '{}', bucket_policy: '{}', env_keys: [] }),
|
||||
},
|
||||
}));
|
||||
|
||||
import { api } from '../../api/client';
|
||||
|
||||
const baseServer = {
|
||||
public_url: '',
|
||||
stats_retention_hours: 168,
|
||||
build_retention_days: 7,
|
||||
pool_reconnect_seconds: 30,
|
||||
websocket_ping_seconds: 30,
|
||||
max_agents: 500,
|
||||
max_build_size_mb: 50,
|
||||
log_agent_connections: false,
|
||||
log_share_submissions: false,
|
||||
log_pool_traffic: false,
|
||||
strict_wallet_validation: true,
|
||||
dashboard_subtitle: '',
|
||||
open_firewall_on_start: false,
|
||||
aws_s3_shard_bucket: 'b',
|
||||
aws_cloudfront_domain: 'd.cf.net',
|
||||
};
|
||||
|
||||
describe('AwsErasureSwarmPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders bucket and CloudFront inputs', () => {
|
||||
render(<AwsErasureSwarmPanel server={baseServer} onServerChange={() => {}} />);
|
||||
expect(screen.getByDisplayValue('b')).toBeTruthy();
|
||||
expect(screen.getByDisplayValue('d.cf.net')).toBeTruthy();
|
||||
expect(screen.getByText(/AWS Erasure Swarm/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('calls test endpoint on Test connection', async () => {
|
||||
render(<AwsErasureSwarmPanel server={baseServer} onServerChange={() => {}} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /Test connection/i }));
|
||||
await waitFor(() => {
|
||||
expect(api.testErasureSwarm).toHaveBeenCalledWith({ s3_bucket: 'b', cloudfront_domain: 'd.cf.net' });
|
||||
});
|
||||
});
|
||||
|
||||
it('loads policy JSON on button click', async () => {
|
||||
render(<AwsErasureSwarmPanel server={baseServer} onServerChange={() => {}} />);
|
||||
fireEvent.click(screen.getByRole('button', { name: /Generate policy JSON/i }));
|
||||
await waitFor(() => {
|
||||
expect(api.getErasureSwarmPolicyJSON).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
87
server/web/src/components/Forge/AwsErasureSwarmPanel.tsx
Normal file
87
server/web/src/components/Forge/AwsErasureSwarmPanel.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useState } from 'react';
|
||||
import { api } from '../../api/client';
|
||||
import type { ServerSettings } from '../../types';
|
||||
import { HelpTip } from '../HelpTip';
|
||||
|
||||
type Props = {
|
||||
server: ServerSettings;
|
||||
onServerChange: (patch: Partial<ServerSettings>) => void;
|
||||
};
|
||||
|
||||
export default function AwsErasureSwarmPanel({ server, onServerChange }: Props) {
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testMsg, setTestMsg] = useState('');
|
||||
const [policyJSON, setPolicyJSON] = useState('');
|
||||
|
||||
const bucket = server.aws_s3_shard_bucket ?? '';
|
||||
const cfDomain = server.aws_cloudfront_domain ?? '';
|
||||
|
||||
const saveFields = async (patch: Partial<ServerSettings>) => {
|
||||
onServerChange(patch);
|
||||
await api.updateConfig({ server: { ...server, ...patch } });
|
||||
};
|
||||
|
||||
const runTest = async () => {
|
||||
setTesting(true);
|
||||
setTestMsg('');
|
||||
try {
|
||||
const res = await api.testErasureSwarm({ s3_bucket: bucket, cloudfront_domain: cfDomain });
|
||||
setTestMsg(res.ok ? 'S3 head bucket OK — credentials and signing ready.' : (res.error || 'Test failed'));
|
||||
} catch (e) {
|
||||
setTestMsg(e instanceof Error ? e.message : 'Test failed');
|
||||
} finally {
|
||||
setTesting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadPolicy = async () => {
|
||||
setPolicyJSON('');
|
||||
try {
|
||||
const res = await api.getErasureSwarmPolicyJSON(bucket);
|
||||
setPolicyJSON(JSON.stringify(res, null, 2));
|
||||
} catch (e) {
|
||||
setPolicyJSON(e instanceof Error ? e.message : 'Failed to load policy JSON');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form-group card operator-deck-card" style={{ marginTop: '1rem' }}>
|
||||
<h3 className="font-tech">AWS Erasure Swarm</h3>
|
||||
<p className="form-hint">
|
||||
Upload RS 4+2 shards to S3 on deploy plans; agents fetch LAN → CloudFront → C2.
|
||||
Credentials via env: <code>AF_AWS_*</code>, <code>AF_CLOUDFRONT_*</code>.
|
||||
<HelpTip field="aws_erasure_swarm" />
|
||||
</p>
|
||||
<label className="label">S3 shard bucket</label>
|
||||
<input
|
||||
className="input"
|
||||
value={bucket}
|
||||
onChange={(e) => onServerChange({ aws_s3_shard_bucket: e.target.value })}
|
||||
onBlur={() => saveFields({ aws_s3_shard_bucket: bucket })}
|
||||
placeholder="my-fleet-shards"
|
||||
/>
|
||||
<label className="label" style={{ marginTop: '0.5rem' }}>CloudFront domain</label>
|
||||
<input
|
||||
className="input"
|
||||
value={cfDomain}
|
||||
onChange={(e) => onServerChange({ aws_cloudfront_domain: e.target.value })}
|
||||
onBlur={() => saveFields({ aws_cloudfront_domain: cfDomain })}
|
||||
placeholder="d123abc.cloudfront.net"
|
||||
/>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<button type="button" className="btn btn-secondary" disabled={testing} onClick={runTest}>
|
||||
{testing ? 'Testing…' : 'Test connection'}
|
||||
</button>
|
||||
<button type="button" className="btn btn-secondary" onClick={loadPolicy}>
|
||||
Generate policy JSON
|
||||
</button>
|
||||
</div>
|
||||
{testMsg && <p className="form-hint" style={{ marginTop: '0.5rem' }}>{testMsg}</p>}
|
||||
{policyJSON && (
|
||||
<pre className="form-hint" style={{ marginTop: '0.5rem', maxHeight: '12rem', overflow: 'auto', fontSize: '0.75rem' }}>
|
||||
{policyJSON}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user