Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ PulpUI is a community driven single-page application that talks to a running [pu
### Plugins
- Ansible (repositories, remotes, collections, namespaces, approvals, imports)
- File (repositories, remotes)
- RPM (content)
- RPM (repositories, remotes, content)
- Container (execution environments, tags, manifests)

## Requirements
Expand Down
25 changes: 25 additions & 0 deletions cypress/e2e/smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,31 @@ describe('UI smoke tests', () => {
// TODO
});

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

cy.contains('No repositories yet');

// pulp_rpm repositories carry publishing and retention settings the other
// plugins have no use for
cy.contains('button', 'Add repository').click();
cy.get('#autopublish');
cy.get('#retain_package_versions');
});

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

cy.contains('No remotes yet');

// the download policy decides whether packages are fetched during sync or
// on first request, so an RPM remote offers it in the form
cy.contains('button', 'Add remote').click();
cy.get('#policy');
});

it('RPMs', () => {
cy.ui('rpm/rpms');
cy.assertTitle('Packages');
Expand Down
7 changes: 7 additions & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ export { fileRepositoryCreateAction } from './file-repository-create';
export { fileRepositoryDeleteAction } from './file-repository-delete';
export { fileRepositoryEditAction } from './file-repository-edit';
export { fileRepositorySyncAction } from './file-repository-sync';
export { rpmRemoteCreateAction } from './rpm-remote-create';
export { rpmRemoteDeleteAction } from './rpm-remote-delete';
export { rpmRemoteEditAction } from './rpm-remote-edit';
export { rpmRepositoryCreateAction } from './rpm-repository-create';
export { rpmRepositoryEditAction } from './rpm-repository-edit';
export { rpmRepositoryDeleteAction } from './rpm-repository-delete';
export { rpmRepositorySyncAction } from './rpm-repository-sync';
9 changes: 9 additions & 0 deletions src/actions/rpm-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 rpmRemoteCreateAction = Action({
title: msg`Add remote`,
onClick: (item, { navigate }) =>
navigate(formatPath(Paths.rpm.remote.edit, { name: '_' })),
});
44 changes: 44 additions & 0 deletions src/actions/rpm-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 { RPMRemoteAPI } from 'src/api';

Check warning on line 2 in src/actions/rpm-remote-delete.tsx

View workflow job for this annotation

GitHub Actions / pr-checks

'src/api' import is restricted from being used. Import the specific file and not the barrel
import { DeleteRemoteModal } from 'src/components';
import {

Check warning on line 4 in src/actions/rpm-remote-delete.tsx

View workflow job for this annotation

GitHub Actions / pr-checks

'src/utilities' import is restricted from being used. Import the specific file and not the barrel
handleHttpError,
parsePulpIDFromURL,
taskAlert,
waitForTaskUrl,
} from 'src/utilities';
import { Action } from './action';

export const rpmRemoteDeleteAction = 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 RPMRemoteAPI.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/rpm-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 rpmRemoteEditAction = Action({
title: msg`Edit`,
onClick: ({ name }, { navigate }) =>
navigate(formatPath(Paths.rpm.remote.edit, { name })),
});
9 changes: 9 additions & 0 deletions src/actions/rpm-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 rpmRepositoryCreateAction = Action({
title: msg`Add repository`,
onClick: (item, { navigate }) =>
navigate(formatPath(Paths.rpm.repository.edit, { name: '_' })),
});
123 changes: 123 additions & 0 deletions src/actions/rpm-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 { RPMDistributionAPI, RPMRepositoryAPI } from 'src/api';

Check warning on line 2 in src/actions/rpm-repository-delete.tsx

View workflow job for this annotation

GitHub Actions / pr-checks

'src/api' import is restricted from being used. Import the specific file and not the barrel
import { DeleteRepositoryModal } from 'src/components';
import {

Check warning on line 4 in src/actions/rpm-repository-delete.tsx

View workflow job for this annotation

GitHub Actions / pr-checks

'src/utilities' import is restricted from being used. Import the specific file and not the barrel
handleHttpError,
parsePulpIDFromURL,
taskAlert,
waitForTaskUrl,
} from 'src/utilities';
import { Action } from './action';

export const rpmRepositoryDeleteAction = 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 RPMDistributionAPI.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 = RPMRepositoryAPI.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 RPMDistributionAPI.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/rpm-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 rpmRepositoryEditAction = Action({
title: msg`Edit`,
onClick: ({ name }, { navigate }) =>
navigate(formatPath(Paths.rpm.repository.edit, { name })),
});
72 changes: 72 additions & 0 deletions src/actions/rpm-repository-sync.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { msg, t } from '@lingui/core/macro';
import { RPMRepositoryAPI } from 'src/api';
import { SyncModal } from 'src/components';
import { handleHttpError, parsePulpIDFromURL, taskAlert } from 'src/utilities';
import { Action } from './action';

// pulp_rpm's own API default, which never removes content. Mirroring is
// opted into, never defaulted to.
const DEFAULT_SYNC_POLICY = 'additive';

const syncPolicies = () => [
{ id: 'additive', label: t`Additive: only add new content` },
{ id: 'mirror_content_only', label: t`Mirror content only` },
{
id: 'mirror_complete',
label: t`Mirror complete (exact copy incl. metadata)`,
},
];

export const rpmRepositorySyncAction = Action({
title: msg`Sync`,
modal: ({ addAlert, query, setState, state }) =>
state.syncModalOpen ? (
<SyncModal
closeAction={() => setState({ syncModalOpen: null })}
defaultSyncPolicy={DEFAULT_SYNC_POLICY}
syncPolicies={syncPolicies()}
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('rpm.change_rpmrepository'),
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 RPMRepositoryAPI.sync(pulpId, {
sync_policy: syncParams?.sync_policy || DEFAULT_SYNC_POLICY,
optimize: syncParams?.optimize ?? true,
})
.then(({ data }) => {
addAlert(taskAlert(data.task, t`Sync started for repository "${name}".`));

query();
})
.catch(
handleHttpError(
t`Failed to sync repository "${name}"`,
() => null,
addAlert,
),
);
}
4 changes: 3 additions & 1 deletion src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ export {
} from './response-types/user';
export { RoleAPI } from './role';
export { RPMPackageAPI } from './rpm-package';
export { RPMRepositoryAPI } from './rpm-repository';
export { RPMDistributionAPI } from './rpm-distribution';
export { RPMRemoteAPI, type RPMRemoteType } from './rpm-remote';
export { RPMRepositoryAPI, type RPMRepositoryType } from './rpm-repository';
export { SignCollectionAPI } from './sign-collections';
export { SignContainersAPI } from './sign-containers';
export { SigningServiceAPI, type SigningServiceType } from './signing-service';
Expand Down
11 changes: 11 additions & 0 deletions src/api/rpm-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 RPMDistributionAPI = {
create: (data) => base.http.post(`distributions/rpm/rpm/`, data),

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

list: (params?) => base.list(`distributions/rpm/rpm/`, params),
};
Loading
Loading