Skip to content
Merged
21 changes: 21 additions & 0 deletions cypress/e2e/smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,27 @@ describe('UI smoke tests', () => {
// TODO
});

it('Deb repositories', () => {
cy.ui('deb/repositories');
cy.assertTitle('Repositories');

cy.contains('No repositories yet');
});

it('Deb remotes', () => {
cy.ui('deb/remotes');
cy.assertTitle('Remotes');

cy.contains('No remotes yet');

// an apt remote cannot sync without being told which suites to fetch, so the
// form carries fields the other plugins have no use for
cy.contains('button', 'Add remote').click();
cy.get('#distributions');
cy.get('#components');
cy.get('#architectures');
});

it('File repositories', () => {
cy.ui('file/repositories');
cy.assertTitle('Repositories');
Expand Down
9 changes: 9 additions & 0 deletions src/actions/deb-remote-create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { msg } from '@lingui/core/macro';
import { Paths, formatPath } from 'src/paths';
import { Action } from './action';

export const debRemoteCreateAction = Action({
title: msg`Add remote`,
onClick: (item, { navigate }) =>
navigate(formatPath(Paths.deb.remote.edit, { name: '_' })),
});
44 changes: 44 additions & 0 deletions src/actions/deb-remote-delete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { msg, t } from '@lingui/core/macro';
import { DebRemoteAPI } from 'src/api';
import { DeleteRemoteModal } from 'src/components';
import {
handleHttpError,
parsePulpIDFromURL,
taskAlert,
waitForTaskUrl,
} from 'src/utilities';
import { Action } from './action';

export const debRemoteDeleteAction = Action({
title: msg`Delete`,
modal: ({ addAlert, listQuery, setState, state }) =>
state.deleteModalOpen ? (
<DeleteRemoteModal
closeAction={() => setState({ deleteModalOpen: null })}
deleteAction={() =>
deleteRemote(state.deleteModalOpen, { addAlert, setState, listQuery })
}
name={state.deleteModalOpen.name}
/>
) : null,
onClick: (
{ name, id, pulp_href }: { name: string; id?: string; pulp_href?: string },
{ setState },
) =>
setState({
deleteModalOpen: { pulpId: id || parsePulpIDFromURL(pulp_href), name },
}),
});

function deleteRemote({ name, pulpId }, { addAlert, setState, listQuery }) {
return DebRemoteAPI.delete(pulpId)
.then(({ data }) => {
addAlert(taskAlert(data.task, t`Removal started for remote ${name}`));
setState({ deleteModalOpen: null });
return waitForTaskUrl(data.task);
})
.then(() => listQuery())
.catch(
handleHttpError(t`Failed to remove remote ${name}`, () => null, addAlert),
);
}
9 changes: 9 additions & 0 deletions src/actions/deb-remote-edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { msg } from '@lingui/core/macro';
import { Paths, formatPath } from 'src/paths';
import { Action } from './action';

export const debRemoteEditAction = Action({
title: msg`Edit`,
onClick: ({ name }, { navigate }) =>
navigate(formatPath(Paths.deb.remote.edit, { name })),
});
9 changes: 9 additions & 0 deletions src/actions/deb-repository-create.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { msg } from '@lingui/core/macro';
import { Paths, formatPath } from 'src/paths';
import { Action } from './action';

export const debRepositoryCreateAction = Action({
title: msg`Add repository`,
onClick: (item, { navigate }) =>
navigate(formatPath(Paths.deb.repository.edit, { name: '_' })),
});
123 changes: 123 additions & 0 deletions src/actions/deb-repository-delete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { msg, t } from '@lingui/core/macro';
import { DebDistributionAPI, DebRepositoryAPI } from 'src/api';
import { DeleteRepositoryModal } from 'src/components';
import {
handleHttpError,
parsePulpIDFromURL,
taskAlert,
waitForTaskUrl,
} from 'src/utilities';
import { Action } from './action';

export const debRepositoryDeleteAction = Action({
title: msg`Delete`,
modal: ({ addAlert, listQuery, setState, state }) =>
state.deleteModalOpen ? (
<DeleteRepositoryModal
closeAction={() => setState({ deleteModalOpen: null })}
deleteAction={() =>
deleteRepository(state.deleteModalOpen, {
addAlert,
listQuery,
setState,
})
}
name={state.deleteModalOpen.name}
/>
) : null,
onClick: (
{ name, id, pulp_href }: { name: string; id?: string; pulp_href?: string },
{ setState },
) =>
setState({
deleteModalOpen: {
pulpId: id || parsePulpIDFromURL(pulp_href),
name,
pulp_href,
},
}),
});

const DISTRIBUTION_PAGE_SIZE = 100;

// A repository can be serving more distributions than a single page holds, and
// any the lookup misses are left pointing at a repository that no longer exists.
async function listDistributions(repository) {
const distributions = [];
let page = 1;
let count = Infinity;

while (distributions.length < count) {
const { data } = await DebDistributionAPI.list({
repository,
page,
page_size: DISTRIBUTION_PAGE_SIZE,
});

// Also stops the loop should count ever disagree with what the pages return.
if (!data.results?.length) {
break;
}

distributions.push(...data.results);
count = data.count;
page++;
}

return distributions;
}

async function deleteRepository(
{ name, pulp_href, pulpId },
{ addAlert, setState, listQuery },
) {
const distributionsToDelete = await listDistributions(pulp_href).catch(
(e) => {
handleHttpError(
t`Failed to list distributions, removing only the repository.`,
() => null,
addAlert,
)(e);
return [];
},
);

const deleteRepo = DebRepositoryAPI.delete(pulpId)
.then(({ data }) => {
addAlert(taskAlert(data.task, t`Removal started for repository ${name}`));
return waitForTaskUrl(data.task);
})
.catch(
handleHttpError(
t`Failed to remove repository ${name}`,
() => setState({ deleteModalOpen: null }),
addAlert,
),
);

const deleteDistribution = ({ name, pulp_href }) => {
const distribution_id = parsePulpIDFromURL(pulp_href);
return DebDistributionAPI.delete(distribution_id)
.then(({ data }) => {
addAlert(
taskAlert(data.task, t`Removal started for distribution ${name}`),
);
return waitForTaskUrl(data.task);
})
.catch(
handleHttpError(
t`Failed to remove distribution ${name}`,
() => null,
addAlert,
),
);
};

return Promise.all([
deleteRepo,
...distributionsToDelete.map(deleteDistribution),
]).then(() => {
setState({ deleteModalOpen: null });
listQuery();
});
}
9 changes: 9 additions & 0 deletions src/actions/deb-repository-edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { msg } from '@lingui/core/macro';
import { Paths, formatPath } from 'src/paths';
import { Action } from './action';

export const debRepositoryEditAction = Action({
title: msg`Edit`,
onClick: ({ name }, { navigate }) =>
navigate(formatPath(Paths.deb.repository.edit, { name })),
});
64 changes: 64 additions & 0 deletions src/actions/deb-repository-sync.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { msg, t } from '@lingui/core/macro';
Comment thread
warisshaikh1 marked this conversation as resolved.
import { DebRepositoryAPI } from 'src/api';
import { SyncModal } from 'src/components';
import { handleHttpError, parsePulpIDFromURL, taskAlert } from 'src/utilities';
import { Action } from './action';

// pulp_deb's own API default for a sync. ansible and file offer to mirror
// instead, and this deliberately does not follow them: mirroring deletes local
// content the remote no longer has, so it is the direction to opt into rather
// than out of.
const MIRROR_BY_DEFAULT = false;

export const debRepositorySyncAction = Action({
title: msg`Sync`,
modal: ({ addAlert, query, setState, state }) =>
state.syncModalOpen ? (
<SyncModal
closeAction={() => setState({ syncModalOpen: null })}
defaultMirror={MIRROR_BY_DEFAULT}
syncAction={(syncParams) =>
syncRepository(state.syncModalOpen, { addAlert, query }, syncParams)
}
name={state.syncModalOpen.name}
/>
) : null,
onClick: ({ name, pulp_href }, { setState }) =>
setState({
syncModalOpen: { name, pulp_href },
}),
visible: (_item, { hasPermission }) =>
hasPermission('deb.change_aptrepository'),
disabled: ({ remote, last_sync_task }) => {
if (!remote) {
return t`There are no remotes associated with this repository.`;
}

if (
last_sync_task &&
['running', 'waiting'].includes(last_sync_task.state)
) {
return t`Sync task is already queued.`;
}
},
});

function syncRepository({ name, pulp_href }, { addAlert, query }, syncParams) {
const pulpId = parsePulpIDFromURL(pulp_href);
return DebRepositoryAPI.sync(
pulpId,
syncParams || { mirror: MIRROR_BY_DEFAULT },
)
.then(({ data }) => {
addAlert(taskAlert(data.task, t`Sync started for repository "${name}".`));

query();
})
.catch(
handleHttpError(
t`Failed to sync repository "${name}"`,
() => null,
addAlert,
),
);
}
7 changes: 7 additions & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ export { ansibleRepositoryDeleteAction } from './ansible-repository-delete';
export { ansibleRepositoryEditAction } from './ansible-repository-edit';
export { ansibleRepositorySyncAction } from './ansible-repository-sync';
export { ansibleRepositoryVersionRevertAction } from './ansible-repository-version-revert';
export { debRemoteCreateAction } from './deb-remote-create';
export { debRemoteDeleteAction } from './deb-remote-delete';
export { debRemoteEditAction } from './deb-remote-edit';
export { debRepositoryCreateAction } from './deb-repository-create';
export { debRepositoryDeleteAction } from './deb-repository-delete';
export { debRepositoryEditAction } from './deb-repository-edit';
export { debRepositorySyncAction } from './deb-repository-sync';
export { fileRemoteCreateAction } from './file-remote-create';
export { fileRemoteDeleteAction } from './file-remote-delete';
export { fileRemoteEditAction } from './file-remote-edit';
Expand Down
11 changes: 11 additions & 0 deletions src/api/deb-distribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { PulpAPI } from './pulp';

const base = new PulpAPI();

export const DebDistributionAPI = {
create: (data) => base.http.post(`distributions/deb/apt/`, data),

delete: (id) => base.http.delete(`distributions/deb/apt/${id}/`),

list: (params?) => base.list(`distributions/deb/apt/`, params),
};
71 changes: 71 additions & 0 deletions src/api/deb-remote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { PulpAPI } from './pulp';

export interface DebRemoteType {
architectures: string;
ca_cert: string;
client_cert: string;
components: string;
distributions: string;
download_concurrency: number;
gpgkey: string;
ignore_missing_package_indices?: boolean;
name: string;
proxy_url: string;
pulp_href?: string;
rate_limit: number;
sync_installer?: boolean;
sync_sources?: boolean;
sync_udebs?: boolean;
tls_validation: boolean;
url: string;

// connect_timeout
// headers
// max_retries
// policy
// prn
// pulp_created
// pulp_labels
// pulp_last_updated
// sock_connect_timeout
// sock_read_timeout
// total_timeout

hidden_fields: {
is_set: boolean;
name: string;
}[];

my_permissions?: string[];
}

// as in file-remote
function smartUpdate(remote: DebRemoteType, unmodifiedRemote: DebRemoteType) {
for (const field of Object.keys(remote)) {
if (remote[field] === '') {
remote[field] = null;
}

// API returns headers:null but doesn't accept it .. and we don't edit headers
if (remote[field] === null && unmodifiedRemote[field] === null) {
delete remote[field];
}
}

return remote;
}

const base = new PulpAPI();

export const DebRemoteAPI = {
create: (data) => base.http.post(`remotes/deb/apt/`, data),

delete: (id) => base.http.delete(`remotes/deb/apt/${id}/`),

get: (id) => base.http.get(`remotes/deb/apt/${id}/`),

list: (params?) => base.list(`remotes/deb/apt/`, params),

smartUpdate: (id, newValue: DebRemoteType, oldValue: DebRemoteType) =>
base.http.put(`remotes/deb/apt/${id}/`, smartUpdate(newValue, oldValue)),
};
Loading
Loading