Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update docker alias migrations #1589

Closed
wants to merge 20 commits into from
Closed
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
37 changes: 29 additions & 8 deletions packages/dappmanager/src/domains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function stripBadDomainChars(s: string): string {

/**
* - Strip container prefix
* - Strip .dappnode, .eth, .dnp
* - Strip .dappnode, .eth, .dnp, .public
* - Strip "_"
*
* @param name "bitcoin.dnp.dappnode.eth"
Expand All @@ -28,21 +28,42 @@ export function shortUniqueDappnodeEns(dnpName: string): string {

export type ContainerNames = { serviceName: string; dnpName: string };

export function getContainerAlias(container: ContainerNames): string {
if (!container.serviceName || !container.dnpName) {
throw Error("serviceName and dnpName are required when setting alias")
}
else {
const alias = `${container.serviceName}.${container.dnpName}`;
return `${shortUniqueDappnodeEns(alias)}.dappnode`;
}
}

export function getContainerRootAlias(dnpName: string): string {
return `${shortUniqueDappnodeEns(dnpName)}.dappnode`;
}

/**
* Returns base alias for a container of the dncore_network.
* - If the container is part of a multiservice package the alias will be "service1.example.dappnode"
* - If the container is part of a mono service package, the alias will be "example.dappnode"
* @param serviceName "beacon-chain" @param dnpName "prysm.dnp.dappnode.eth"
* @returns
* - "beacon-chain.prysm.dappnode"
*/
export function getPrivateNetworkAlias(container: ContainerNames): string {
const fullEns = getContainerDomain(container);
return `${shortUniqueDappnodeEns(fullEns)}.dappnode`;
}

export function getPrivateNetworkAliases(
container: ContainerNames & { isMain: boolean }
container: ContainerNames & { isMainOrMonoService: boolean }
): string[] {
const aliases: string[] = [getPrivateNetworkAlias(container)];
const aliases: string[] = [getContainerAlias(container)];

if (container.isMain) {
const rootAlias = getPrivateNetworkAlias({
dnpName: container.dnpName,
serviceName: container.dnpName
});
// mono services will always be main.
// If mono service or multiserviceMain, add the root alias (alias without service name)
if (container.isMainOrMonoService) {
const rootAlias = getContainerRootAlias(container.dnpName);
aliases.push(rootAlias);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function setDappnodeComposeDefaults(
serviceName,
dnpName,
// The root pkg alias will be added to the main service or if it is a mono service
isMain: isMonoService || manifest.mainService === serviceName
isMainOrMonoService: isMonoService || manifest.mainService === serviceName
})
});
}
Expand Down Expand Up @@ -94,7 +94,7 @@ function ensureMinimumComposeVersion(composeFileVersion: string): string {
*/
function setServiceNetworksWithAliases(
serviceNetworks: ComposeServiceNetworks | undefined,
service: { serviceName: string; dnpName: string; isMain: boolean }
service: { serviceName: string; dnpName: string; isMainOrMonoService: boolean }
): ComposeServiceNetworks {
// Return service network dncore_network with aliases if not provided
if (!serviceNetworks)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ComposeNetwork, ComposeServiceNetwork } from "@dappnode/types";
import Dockerode from "dockerode";
import { uniq } from "lodash-es";
import { PackageContainer } from "@dappnode/common";
import { getPrivateNetworkAlias } from "../../domains.js";
import { getPrivateNetworkAliases } from "../../domains.js";
import { logs } from "@dappnode/logger";
import { params } from "@dappnode/params";
import { parseComposeSemver } from "../../utils/sanitizeVersion.js";
Expand All @@ -26,140 +26,164 @@ const dncoreNetworkName = params.DNP_PRIVATE_NETWORK_NAME;
* DAPPMANAGER updates from <= v0.2.38 must manually add aliases
* to all running containers.
* This will run every single time dappmanager restarts and will list al packages
* and do docker inspect.
* and do docker inspect. This migration tries to assure that:
* Having a package name "example.dnp.dappnode.eth" the aliases should be:
* "example.dappnode" if the package is mono service
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we additionally supported service.example.dappnode for mono service packages?
@Marketen @3alpha

* "service1.example.dappnode" if the package is multiservice
* "service1.example.dappnode" and "example.dappnode" if the package is multiservice and has in manifest mainservice
*/
export async function addAliasToRunningContainers(): Promise<void> {
for (const container of await listContainers()) {
const containerName = container.containerName;
const alias = getPrivateNetworkAlias(container);

try {
// Info from docker inspect and compose file might be not-syncrhnonyzed
// So this function must be before the check hasAlias()
migrateCoreNetworkAndAliasInCompose(container, alias);

const currentEndpointConfig = await getDnCoreNetworkContainerConfig(
containerName
);
if (hasAlias(currentEndpointConfig, alias)) continue;
const endpointConfig: Partial<Dockerode.NetworkInfo> = {
...currentEndpointConfig,
Aliases: [...(currentEndpointConfig?.Aliases || []), alias]
};

// Wifi and VPN containers needs a refresh connect due to its own network configuration
if (
container.containerName === params.vpnContainerName ||
container.containerName === params.wifiContainerName
) {
await shell(`docker rm ${containerName} --force`);
await dockerComposeUp(
getPath.dockerCompose(container.dnpName, container.isCore)
);
} else {
await dockerNetworkDisconnect(dncoreNetworkName, containerName);
await dockerNetworkConnect(
dncoreNetworkName,
containerName,
endpointConfig
);
}
logs.info(`Added alias to running container ${container.containerName}`);
} catch (e) {
logs.error(`Error adding alias to container ${containerName}`, e);
}
try {
const containers = await listContainers();
await addAliasToGivenContainers(containers);
} catch (error) {
logs.error('Error adding alias to running containers:', error);
}
}

/** Return true if endpoint config exists and has alias */
function hasAlias(
endpointConfig: Dockerode.NetworkInfo | null,
alias: string
): boolean {
return Boolean(
endpointConfig &&
endpointConfig.Aliases &&
Array.isArray(endpointConfig.Aliases) &&
endpointConfig.Aliases.includes(alias)
);
export async function addAliasToGivenContainers(containers: PackageContainer[]): Promise<void> {
for (const container of containers) {

const isMainOrMonoService = container.isMain ?? false; // Set a default value of false if isMain is undefined
const service = { serviceName: container.serviceName, dnpName: container.dnpName, isMainOrMonoService }
const aliases = getPrivateNetworkAliases(service)

// Adds aliases to the compose file that generated the container
migrateCoreNetworkAndAliasInCompose(container, aliases);

// Adds aliases to the container network
for (const alias of aliases) {
const currentEndpointConfig = await getDnCoreNetworkContainerConfig(container.containerName);
if (!hasAlias(currentEndpointConfig, alias)) {
const updatedConfig = updateEndpointConfig(currentEndpointConfig, alias);
await updateContainerNetwork(dncoreNetworkName, container, updatedConfig);
logs.info(`alias ${alias} added to ${container.containerName}`);
}
}
}
}

/**
* Get compose file network and compose network settings from dncore_network
* And rewrites the compose with the core network edited
/** Gets the docker-compose.yml file of the given `container` and adds one or more alias
* to the service that started `container`. All alias are added to the network defined by
* `params.DNP_PRIVATE_NETWORK_NAME`.
*
* @param container PackageContainer
* @param aliases string[]
* @returns void
*/
export function migrateCoreNetworkAndAliasInCompose(
container: PackageContainer,
alias: string
aliases: string[]
): void {
const compose = new ComposeFileEditor(container.dnpName, container.isCore);

// 1. Get compose network settings
const composeNetwork = compose.getComposeNetwork(
params.DNP_PRIVATE_NETWORK_NAME
);

// 2. Get compose service network settings
const composeService = compose.services()[container.serviceName];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked this better, looks cleaner, but as you prefer


// Gets all the networks defined in the service
const serviceNetworks = parseServiceNetworks(
composeService.get().networks || {}
compose.services()[container.serviceName].get().networks || {}
);

// Gets current aliases of "params.DNP_PRIVATE_NETWORK_NAME", usually dncore_network
const currentAliases = serviceNetworks[params.DNP_PRIVATE_NETWORK_NAME]?.aliases || [];

const serviceNetwork =
serviceNetworks[params.DNP_PRIVATE_NETWORK_NAME_FROM_CORE] ?? null;
//add new aliases to current aliases set
const newAliases = uniq([...currentAliases, ...aliases]);

// 3. Check if migration was done
if (
isComposeNetworkAndAliasMigrated(
composeNetwork,
serviceNetwork,
compose.compose.version,
alias
)
)
return;
// Check if migration was done
const composeNetwork = compose.getComposeNetwork(params.DNP_PRIVATE_NETWORK_NAME);
const serviceNetwork = serviceNetworks[params.DNP_PRIVATE_NETWORK_NAME] ?? null;

// 4. Ensure compose file version 3.5
// Return if migration was done, compose is already updated
if (isComposeNetworkAndAliasMigrated(composeNetwork, serviceNetwork, compose.compose.version, newAliases)) return
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return? I think you should log the error (throw an exception? investigate on where would it be catched)


// Ensure/update compose file version 3.5
compose.compose = {
...compose.compose,
version: params.MINIMUM_COMPOSE_VERSION
};

// 5. Add network and alias
if (composeNetwork || serviceNetwork)
// composeNetwork and serviceNetwork might be null and have different values (eitherway it should be the same)
// Only remove network if exists
composeService.removeNetwork(params.DNP_PRIVATE_NETWORK_NAME_FROM_CORE);

const aliases = uniq([...(serviceNetwork?.aliases || []), alias]);
composeService.addNetwork(
// This adds the new network with the new aliases into the compose file
compose.services()[container.serviceName].addNetwork(
params.DNP_PRIVATE_NETWORK_NAME,
{ ...serviceNetwork, aliases },
{ external: true, name: params.DNP_PRIVATE_NETWORK_NAME } //...networkConfig,
{ ...serviceNetwork, aliases: newAliases },
{ external: true, name: params.DNP_PRIVATE_NETWORK_NAME }
);

compose.write();
}

// function isMainServiceOfMultiServicePackage(container: PackageContainer): boolean {
// const compose = new ComposeFileEditor(container.dnpName, container.isCore);
// const services = compose.services(); // Invoke the services function
// if (Object.keys(services).length > 1 && container.isMain) return true;
// return false;
// }

function updateEndpointConfig(currentEndpointConfig: Dockerode.NetworkInfo | null, alias: string) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call this function addAliasToEndpointConfig

return {
...currentEndpointConfig,
Aliases: [...(currentEndpointConfig?.Aliases || []), alias]
};
}

async function updateContainerNetwork(networkName: string, container: any, endpointConfig: Partial<Dockerode.NetworkInfo>): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe reconnectContainerToNetwork?

const containerName = container.containerName;

// Wifi and VPN containers need a refresh connect due to their own network configuration
if (containerName === params.vpnContainerName || containerName === params.wifiContainerName) {
await shell(`docker rm ${containerName} --force`);
await dockerComposeUp(getPath.dockerCompose(container.dnpName, container.isCore));
} else {
await dockerNetworkDisconnect(networkName, containerName);
console.log(`new alias for: ${containerName}`);
await dockerNetworkConnect(networkName, containerName, endpointConfig);
}
}

/** Return true if endpoint config exists, has an array of Alisases and it contains the alias
* @param alias
* @returns boolean
*/
function hasAlias(
endpointConfig: Dockerode.NetworkInfo | null,
alias: string
): boolean {
return Boolean(
endpointConfig &&
endpointConfig.Aliases &&
Array.isArray(endpointConfig.Aliases) &&
endpointConfig.Aliases.includes(alias)
);
}

/** Return true if docker-compose.yml file has already been updated with aliases, false otherwise.
* @param aliases
* @returns boolean
*/
function isComposeNetworkAndAliasMigrated(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could divide this into:
isComposeNetworkMigrated
isAliasMigrated

This way you would keep the aim of each function unique and error logging would be more verbose

composeNetwork: ComposeNetwork | null,
serviceNetwork: ComposeServiceNetwork | null,
composeVersion: string,
alias: string
aliases: string[]
): boolean {
// 1. Migration undone for aliases or networks or both => return false
if (!composeNetwork || !serviceNetwork) return false; // Consider as not migrated if either composeNetwork or serviceNetwork are not present
// 2. Migration done for aliases and networks => return true
// 1. Expected network is not present either in compose or in service => not migrated
if (!composeNetwork || !serviceNetwork) return false;

// 2. Aside from being at least version 3.5, to consider the docker-compose.yml file as migrated, the network defined in the compose file must:
// - be external
// - have the expected name
// - have the expected aliases in each service
if (
composeNetwork?.name === params.DNP_PRIVATE_NETWORK_NAME && // Check property name is defined
composeNetwork?.name === params.DNP_PRIVATE_NETWORK_NAME && // Check expected name
composeNetwork?.external && // Check is external network
gte(
parseComposeSemver(composeVersion),
parseComposeSemver(params.MINIMUM_COMPOSE_VERSION)
) && // Check version is at least 3.5
serviceNetwork.aliases?.includes(alias) // Check alias has been added
aliases.every(alias => serviceNetwork.aliases?.includes(alias)) // Check every alias is already present
)
return true;

return false; // In other cases return false
}

5 changes: 0 additions & 5 deletions packages/dappmanager/test/unit/domains.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ describe("domains", () => {
serviceName: "prysm.dnp.dappnode.eth",
domain: "prysm.dappnode"
},
{
dnpName: "prysm.dnp.dappnode.eth",
serviceName: "prysm.dnp.dappnode.eth",
domain: "prysm.dappnode"
},
{
dnpName: "prysm.public.dappnode.eth",
serviceName: "validator",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ describe("setDappnodeComposeDefaults", () => {
networks: {
dncore_network: {
ipv4_address: "172.33.1.7",
aliases: ["dappmanager.dappnode", "my.dappnode", "dappnode.local"]
aliases: ["dappmanager.dnp.dappnode.eth.dappmanager.dappnode", "dappmanager.dappnode", "my.dappnode", "dappnode.local"]
}
}
}
Expand Down Expand Up @@ -247,7 +247,7 @@ describe("setDappnodeComposeDefaults", () => {
networks: {
dncore_network: {
ipv4_address: "172.33.1.7",
aliases: ["dappmanager.dappnode", "my.dappnode", "dappnode.local"]
aliases: ["dappmanager.dnp.dappnode.eth.dappmanager.dappnode", "dappmanager.dappnode", "my.dappnode", "dappnode.local"]
}
},
volumes: [
Expand Down
Loading
Loading