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
48 changes: 48 additions & 0 deletions src/server/services/secret-save.unit.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import secretService from './secret.service';

const { core } = vi.hoisted(() => ({
core: {
readNamespacedSecret: vi.fn(),
replaceNamespacedSecret: vi.fn(),
createNamespacedSecret: vi.fn(),
listNamespacedSecret: vi.fn(),
deleteNamespacedSecret: vi.fn(),
},
}));

vi.mock('@/server/adapter/kubernetes-api.adapter', () => ({ default: { core } }));

describe('saveSecret', () => {
const manifest = { metadata: { name: 'pull-secret' }, data: { token: 'new' } };

beforeEach(() => vi.resetAllMocks());

it('replaces an existing secret using its resource version without deleting it', async () => {
core.readNamespacedSecret.mockResolvedValue({ metadata: { name: 'pull-secret', resourceVersion: '1', labels: { owner: 'app' } } });
await secretService.saveSecret('project', 'pull-secret', manifest);
expect(core.replaceNamespacedSecret).toHaveBeenCalledWith({
namespace: 'project', name: 'pull-secret',
body: { ...manifest, metadata: { name: 'pull-secret', resourceVersion: '1', labels: { owner: 'app' } } },
});
expect(core.deleteNamespacedSecret).not.toHaveBeenCalled();
});

it('retries with the latest resource version after a conflict', async () => {
core.readNamespacedSecret
.mockResolvedValueOnce({ metadata: { name: 'pull-secret', resourceVersion: '1' } })
.mockResolvedValueOnce({ metadata: { name: 'pull-secret', resourceVersion: '2' } });
core.replaceNamespacedSecret.mockRejectedValueOnce({ code: 409 }).mockResolvedValueOnce({});
await secretService.saveSecret('project', 'pull-secret', manifest);
expect(core.replaceNamespacedSecret).toHaveBeenLastCalledWith(expect.objectContaining({
body: expect.objectContaining({ metadata: expect.objectContaining({ resourceVersion: '2' }) }),
}));
});

it('retries when another caller creates the secret first', async () => {
core.readNamespacedSecret.mockRejectedValueOnce({ code: 404 })
.mockResolvedValueOnce({ metadata: { name: 'pull-secret', resourceVersion: '1' } });
core.createNamespacedSecret.mockRejectedValueOnce({ code: 409 });
await secretService.saveSecret('project', 'pull-secret', manifest);
expect(core.replaceNamespacedSecret).toHaveBeenCalledOnce();
});
});
49 changes: 41 additions & 8 deletions src/server/services/secret.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,50 @@ class SecretService {
}

async updateSecret(namespace: string, secretName: string, secretManifest: V1Secret) {
console.log(`Updating secret ${secretName}...`);
await this.deleteSecretIfExists(namespace, secretName);
await this.createSecret(namespace, secretManifest);
await this.saveSecret(namespace, secretName, secretManifest);
}

async saveSecret(namespace: string, secretName: string, secretManifest: V1Secret) {
const existingSecret = await this.getExistingSecret(namespace, secretName);
if (existingSecret) {
await this.updateSecret(namespace, secretName, secretManifest);
} else {
await this.createSecret(namespace, secretManifest);
//Sometimes issues occured, when starting a agent sandbox beecause of race conditions. Yes its ugly, but for now it works. Investiage in future releases.
const maxAttempts = 5;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
let existingSecret: V1Secret;
try {
existingSecret = await k3s.core.readNamespacedSecret({ namespace, name: secretName });
} catch (err) {
if ((err as ApiException<unknown>).code !== 404) throw err;
try {
await this.createSecret(namespace, secretManifest);
return;
} catch (createError) {
if ((createError as ApiException<unknown>).code !== 409 || attempt === maxAttempts - 1) {
throw createError;
}
continue;
}
}

try {
console.log('replacing bitch')
await k3s.core.replaceNamespacedSecret({
namespace,
name: secretName,
body: {
...secretManifest,
metadata: {
...existingSecret.metadata,
...secretManifest.metadata,
resourceVersion: existingSecret.metadata?.resourceVersion,
},
},
});
return;
} catch (err) {
const code = (err as ApiException<unknown>).code;
if ((code !== 409 && code !== 404) || attempt === maxAttempts - 1) {
throw err;
}
}
}
}

Expand Down
Loading