mirror of https://github.com/portainer/portainer
356 lines
8.0 KiB
TypeScript
356 lines
8.0 KiB
TypeScript
import { useMutation, useQueryClient } from 'react-query';
|
|
import { AxiosRequestHeaders } from 'axios';
|
|
|
|
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
|
import {
|
|
Environment,
|
|
EnvironmentId,
|
|
EnvironmentType,
|
|
} from '@/react/portainer/environments/types';
|
|
import {
|
|
Registry,
|
|
RegistryId,
|
|
} from '@/react/portainer/registries/types/registry';
|
|
import { createWebhook } from '@/react/portainer/webhooks/createWebhook';
|
|
import { WebhookType } from '@/react/portainer/webhooks/types';
|
|
import {
|
|
AccessControlFormData,
|
|
ResourceControlResponse,
|
|
} from '@/react/portainer/access-control/types';
|
|
import { applyResourceControl } from '@/react/portainer/access-control/access-control.service';
|
|
import PortainerError from '@/portainer/error';
|
|
import {
|
|
mutationOptions,
|
|
withError,
|
|
withInvalidate,
|
|
} from '@/react-tools/react-query';
|
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
|
|
|
import { pullImage } from '../../images/queries/usePullImageMutation';
|
|
import {
|
|
removeContainer,
|
|
renameContainer,
|
|
startContainer,
|
|
stopContainer,
|
|
urlBuilder,
|
|
} from '../containers.service';
|
|
import { PortainerResponse } from '../../types';
|
|
import { connectContainer } from '../../networks/queries/useConnectContainer';
|
|
import { DockerContainer } from '../types';
|
|
import { queryKeys } from '../queries/query-keys';
|
|
|
|
import { CreateContainerRequest } from './types';
|
|
import { Values } from './useInitialValues';
|
|
|
|
interface ExtraNetwork {
|
|
networkName: string;
|
|
aliases: string[];
|
|
}
|
|
|
|
export function useCreateOrReplaceMutation() {
|
|
const environmentId = useEnvironmentId();
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation(
|
|
createOrReplace,
|
|
mutationOptions(
|
|
withError('Failed to create container'),
|
|
withInvalidate(queryClient, [queryKeys.list(environmentId)])
|
|
)
|
|
);
|
|
}
|
|
|
|
interface CreateOptions {
|
|
config: CreateContainerRequest;
|
|
values: Values;
|
|
registry?: Registry;
|
|
environment: Environment;
|
|
}
|
|
|
|
interface ReplaceOptions extends CreateOptions {
|
|
oldContainer: DockerContainer;
|
|
extraNetworks: Array<ExtraNetwork>;
|
|
}
|
|
|
|
function isReplace(
|
|
options: ReplaceOptions | CreateOptions
|
|
): options is ReplaceOptions {
|
|
return 'oldContainer' in options && !!options.oldContainer;
|
|
}
|
|
|
|
export function createOrReplace(options: ReplaceOptions | CreateOptions) {
|
|
return isReplace(options) ? replace(options) : create(options);
|
|
}
|
|
|
|
async function create({
|
|
config,
|
|
values,
|
|
registry,
|
|
environment,
|
|
}: CreateOptions) {
|
|
await pullImageIfNeeded(
|
|
environment.Id,
|
|
values.nodeName,
|
|
values.alwaysPull,
|
|
values.image.image,
|
|
registry
|
|
);
|
|
|
|
const containerResponse = await createAndStart(
|
|
environment,
|
|
config,
|
|
values.name,
|
|
values.nodeName
|
|
);
|
|
|
|
await applyContainerSettings(
|
|
containerResponse.Id,
|
|
environment,
|
|
values.enableWebhook,
|
|
values.accessControl,
|
|
containerResponse.Portainer?.ResourceControl,
|
|
registry
|
|
);
|
|
}
|
|
|
|
async function replace({
|
|
oldContainer,
|
|
config,
|
|
values,
|
|
registry,
|
|
environment,
|
|
extraNetworks,
|
|
}: ReplaceOptions) {
|
|
await pullImageIfNeeded(
|
|
environment.Id,
|
|
values.nodeName,
|
|
values.alwaysPull,
|
|
values.image.image,
|
|
registry
|
|
);
|
|
|
|
const containerResponse = await renameAndCreate(
|
|
environment,
|
|
values,
|
|
oldContainer,
|
|
config
|
|
);
|
|
|
|
await applyContainerSettings(
|
|
containerResponse.Id,
|
|
environment,
|
|
values.enableWebhook,
|
|
values.accessControl,
|
|
containerResponse.Portainer?.ResourceControl,
|
|
registry
|
|
);
|
|
|
|
await connectToExtraNetworks(
|
|
environment.Id,
|
|
values.nodeName,
|
|
containerResponse.Id,
|
|
extraNetworks
|
|
);
|
|
|
|
await removeContainer(environment.Id, oldContainer.Id, {
|
|
nodeName: values.nodeName,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* stop and renames the old container, and creates and stops the new container.
|
|
* on any failure, it will rename the old container to its original name
|
|
*/
|
|
async function renameAndCreate(
|
|
environment: Environment,
|
|
values: Values,
|
|
oldContainer: DockerContainer,
|
|
config: CreateContainerRequest
|
|
) {
|
|
let renamed = false;
|
|
try {
|
|
await stopContainerIfNeeded(environment.Id, values.nodeName, oldContainer);
|
|
|
|
await renameContainer(
|
|
environment.Id,
|
|
oldContainer.Id,
|
|
`${oldContainer.Names[0]}-old`,
|
|
{ nodeName: values.nodeName }
|
|
);
|
|
renamed = true;
|
|
|
|
return await createAndStart(
|
|
environment,
|
|
config,
|
|
values.name,
|
|
values.nodeName
|
|
);
|
|
} catch (e) {
|
|
if (renamed) {
|
|
await renameContainer(environment.Id, oldContainer.Id, values.name, {
|
|
nodeName: values.nodeName,
|
|
});
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* creates a webhook if necessary and applies resource control
|
|
*/
|
|
async function applyContainerSettings(
|
|
containerId: string,
|
|
environment: Environment,
|
|
enableWebhook: boolean,
|
|
accessControl: AccessControlFormData,
|
|
resourceControl?: ResourceControlResponse,
|
|
registry?: Registry
|
|
) {
|
|
if (enableWebhook) {
|
|
await createContainerWebhook(containerId, environment, registry?.Id);
|
|
}
|
|
|
|
// Portainer will always return a resource control, but since types mark it as optional, we need to check it.
|
|
// Ignoring the missing value will result with bugs, hence it's better to throw an error
|
|
if (!resourceControl) {
|
|
throw new PortainerError('resource control expected after creation');
|
|
}
|
|
|
|
await applyResourceControl(accessControl, resourceControl.Id);
|
|
}
|
|
|
|
/**
|
|
* creates a new container and starts it.
|
|
* on failure, it will remove the new container
|
|
*/
|
|
async function createAndStart(
|
|
environment: Environment,
|
|
config: CreateContainerRequest,
|
|
name: string,
|
|
nodeName: string
|
|
) {
|
|
let containerId = '';
|
|
try {
|
|
const containerResponse = await createContainer(
|
|
environment.Id,
|
|
config,
|
|
name,
|
|
{
|
|
nodeName,
|
|
}
|
|
);
|
|
|
|
containerId = containerResponse.Id;
|
|
|
|
await startContainer(environment.Id, containerResponse.Id, { nodeName });
|
|
return containerResponse;
|
|
} catch (e) {
|
|
if (containerId) {
|
|
await removeContainer(environment.Id, containerId, {
|
|
nodeName,
|
|
});
|
|
}
|
|
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async function pullImageIfNeeded(
|
|
environmentId: EnvironmentId,
|
|
nodeName: string,
|
|
pull: boolean,
|
|
image: string,
|
|
registry?: Registry
|
|
) {
|
|
if (!pull) {
|
|
return null;
|
|
}
|
|
|
|
return pullImage({
|
|
environmentId,
|
|
nodeName,
|
|
image,
|
|
registry,
|
|
ignoreErrors: true,
|
|
});
|
|
}
|
|
|
|
async function createContainer(
|
|
environmentId: EnvironmentId,
|
|
config: CreateContainerRequest,
|
|
name?: string,
|
|
{ nodeName }: { nodeName?: string } = {}
|
|
) {
|
|
try {
|
|
const headers: AxiosRequestHeaders = {};
|
|
|
|
if (nodeName) {
|
|
headers['X-PortainerAgent-Target'] = nodeName;
|
|
}
|
|
|
|
const { data } = await axios.post<
|
|
PortainerResponse<{ Id: string; Warnings: Array<string> }>
|
|
>(urlBuilder(environmentId, undefined, 'create'), config, {
|
|
headers,
|
|
params: { name },
|
|
});
|
|
|
|
return data;
|
|
} catch (err) {
|
|
throw parseAxiosError(err, 'Unable to create container');
|
|
}
|
|
}
|
|
|
|
async function createContainerWebhook(
|
|
containerId: string,
|
|
environment: Environment,
|
|
registryId?: RegistryId
|
|
) {
|
|
const isNotEdgeAgentOnDockerEnvironment =
|
|
environment.Type !== EnvironmentType.EdgeAgentOnDocker;
|
|
if (!isNotEdgeAgentOnDockerEnvironment) {
|
|
return;
|
|
}
|
|
|
|
await createWebhook({
|
|
resourceId: containerId,
|
|
environmentId: environment.Id,
|
|
registryId,
|
|
webhookType: WebhookType.DockerContainer,
|
|
});
|
|
}
|
|
|
|
function connectToExtraNetworks(
|
|
environmentId: EnvironmentId,
|
|
nodeName: string,
|
|
containerId: string,
|
|
extraNetworks: Array<ExtraNetwork>
|
|
) {
|
|
if (!extraNetworks) {
|
|
return null;
|
|
}
|
|
|
|
return Promise.all(
|
|
extraNetworks.map(({ networkName, aliases }) =>
|
|
connectContainer({
|
|
networkId: networkName,
|
|
nodeName,
|
|
containerId,
|
|
environmentId,
|
|
aliases,
|
|
})
|
|
)
|
|
);
|
|
}
|
|
|
|
function stopContainerIfNeeded(
|
|
environmentId: EnvironmentId,
|
|
nodeName: string,
|
|
container: DockerContainer
|
|
) {
|
|
if (container.State !== 'running' || !container.Id) {
|
|
return null;
|
|
}
|
|
return stopContainer(environmentId, container.Id, { nodeName });
|
|
}
|