diff --git a/src/components/common/AdvancedOptionsButton.tsx b/src/components/common/AdvancedOptionsButton.tsx index 943e92ce2..b9f70c2e0 100644 --- a/src/components/common/AdvancedOptionsButton.tsx +++ b/src/components/common/AdvancedOptionsButton.tsx @@ -4,7 +4,7 @@ import { Button, Form } from 'antd'; import { usePrefixedTranslation } from 'hooks'; import { AnyNode } from 'shared/types'; import { useStoreActions } from 'store'; -import { dockerConfigs } from 'utils/constants'; +import { getDefaultCommand } from 'utils/network'; interface Props { node: AnyNode; @@ -18,7 +18,7 @@ const AdvancedOptionsButton: React.FC = ({ node, type }) => { showAdvancedOptions({ nodeName: node.name, command: node.docker.command, - defaultCommand: dockerConfigs[node.implementation].command, + defaultCommand: getDefaultCommand(node.implementation, node.version), }); }; diff --git a/src/components/nodeImages/ManagedImageModal.tsx b/src/components/nodeImages/ManagedImageModal.tsx index 78e3c6e15..3bf937249 100644 --- a/src/components/nodeImages/ManagedImageModal.tsx +++ b/src/components/nodeImages/ManagedImageModal.tsx @@ -6,6 +6,7 @@ import { usePrefixedTranslation } from 'hooks'; import { useStoreActions } from 'store'; import { ManagedImage } from 'types'; import { dockerConfigs } from 'utils/constants'; +import { getDefaultCommand } from 'utils/network'; import { CommandVariables } from './'; const Styled = { @@ -68,7 +69,10 @@ const ManagedImageModal: React.FC = ({ image, onClose }) => { layout="vertical" hideRequiredMark colon={false} - initialValues={{ command: image.command || config.command }} + initialValues={{ + command: + image.command || getDefaultCommand(image.implementation, image.version), + }} onFinish={handleSubmit} > {l('summary')} diff --git a/src/lib/docker/composeFile.spec.ts b/src/lib/docker/composeFile.spec.ts index 4234d90a3..d00b48ff7 100644 --- a/src/lib/docker/composeFile.spec.ts +++ b/src/lib/docker/composeFile.spec.ts @@ -143,13 +143,33 @@ describe('ComposeFile', () => { }); it('should use the tapd nodes custom docker data', () => { - tapNode.docker = { image: 'my-image', command: 'my-command' }; - composeFile.addTapd(tapNode, lndNode); + const tap = { + ...tapNode, + docker: { image: 'my-image', command: 'my-command' }, + }; + composeFile.addTapd(tap, lndNode); const service = composeFile.content.services['alice-tap']; expect(service.image).toBe('my-image'); expect(service.command).toBe('my-command'); }); + it('should use the correct command for tapd v3', () => { + const tap = { ...tapNode, version: '0.3.3' }; + composeFile.addTapd(tap, lndNode); + const service = composeFile.content.services['alice-tap']; + expect(service.command).toContain('--universe.public-access'); + expect(service.command).not.toContain('--universe.public-access=rw'); + expect(service.command).not.toContain('--universe.sync-all-assets'); + }); + + it('should use the correct command for tapd v4+', () => { + const tap = { ...tapNode, version: '0.4.0' }; + composeFile.addTapd(tap, lndNode); + const service = composeFile.content.services['alice-tap']; + expect(service.command).toContain('--universe.public-access=rw'); + expect(service.command).toContain('--universe.sync-all-assets'); + }); + it('should add an litd config', () => { composeFile.addLitd(litdNode, btcNode); expect(composeFile.content.services['dave']).not.toBeUndefined(); diff --git a/src/lib/docker/composeFile.ts b/src/lib/docker/composeFile.ts index 3b1c5fdca..e4c06ce5c 100644 --- a/src/lib/docker/composeFile.ts +++ b/src/lib/docker/composeFile.ts @@ -13,7 +13,7 @@ import { eclairCredentials, litdCredentials, } from 'utils/constants'; -import { getContainerName } from 'utils/network'; +import { getContainerName, getDefaultCommand } from 'utils/network'; import { bitcoind, clightning, eclair, litd, lnd, tapd } from './nodeTemplates'; export interface ComposeService { @@ -71,7 +71,7 @@ class ComposeFile { // use the node's custom image or the default for the implementation const image = node.docker.image || `${dockerConfigs.bitcoind.imageName}:${version}`; // use the node's custom command or the default for the implementation - const nodeCommand = node.docker.command || dockerConfigs.bitcoind.command; + const nodeCommand = node.docker.command || getDefaultCommand('bitcoind', version); // replace the variables in the command const command = this.mergeCommand(nodeCommand, variables); // add the docker service @@ -94,7 +94,7 @@ class ComposeFile { // use the node's custom image or the default for the implementation const image = node.docker.image || `${dockerConfigs.LND.imageName}:${version}`; // use the node's custom command or the default for the implementation - const nodeCommand = node.docker.command || dockerConfigs.LND.command; + const nodeCommand = node.docker.command || getDefaultCommand('LND', version); // replace the variables in the command const command = this.mergeCommand(nodeCommand, variables); // add the docker service @@ -117,7 +117,7 @@ class ComposeFile { const image = node.docker.image || `${dockerConfigs['c-lightning'].imageName}:${version}`; // use the node's custom command or the default for the implementation - let nodeCommand = node.docker.command || dockerConfigs['c-lightning'].command; + let nodeCommand = node.docker.command || getDefaultCommand('c-lightning', version); // do not include the GRPC port arg in the command for unsupported versions if (grpc === 0) nodeCommand = nodeCommand.replace('--grpc-port=11001', ''); // replace the variables in the command @@ -142,7 +142,7 @@ class ComposeFile { // use the node's custom image or the default for the implementation const image = node.docker.image || `${dockerConfigs.eclair.imageName}:${version}`; // use the node's custom command or the default for the implementation - const nodeCommand = node.docker.command || dockerConfigs.eclair.command; + const nodeCommand = node.docker.command || getDefaultCommand('eclair', version); // replace the variables in the command const command = this.mergeCommand(nodeCommand, variables); // add the docker service @@ -166,7 +166,7 @@ class ComposeFile { // use the node's custom image or the default for the implementation const image = node.docker.image || `${dockerConfigs.litd.imageName}:${version}`; // use the node's custom command or the default for the implementation - const nodeCommand = node.docker.command || dockerConfigs.litd.command; + const nodeCommand = node.docker.command || getDefaultCommand('litd', version); // replace the variables in the command const command = this.mergeCommand(nodeCommand, variables); // add the docker service @@ -187,7 +187,7 @@ class ComposeFile { // use the node's custom image or the default for the implementation const image = node.docker.image || `${dockerConfigs.tapd.imageName}:${version}`; // use the node's custom command or the default for the implementation - const nodeCommand = node.docker.command || dockerConfigs.tapd.command; + const nodeCommand = node.docker.command || getDefaultCommand('tapd', version); // replace the variables in the command const command = this.mergeCommand(nodeCommand, variables); // add the docker service diff --git a/src/utils/network.ts b/src/utils/network.ts index bc06967cf..b994305cf 100644 --- a/src/utils/network.ts +++ b/src/utils/network.ts @@ -36,7 +36,7 @@ import { read, rm } from './files'; import { migrateNetworksFile } from './migrations'; import { getName } from './names'; import { range } from './numbers'; -import { isVersionCompatible } from './strings'; +import { isVersionBelow, isVersionCompatible } from './strings'; import { getPolarPlatform } from './system'; import { prefixTranslation } from './translate'; @@ -688,6 +688,26 @@ export const getMissingImages = (network: Network, pulled: string[]): string[] = return unique; }; +/** + * Returns the default docker command for a given implementation and version. This will + * tweak commands for older node versions that do not support certain flags. + */ +export const getDefaultCommand = ( + implementation: NodeImplementation, + version: string, +) => { + let command = dockerConfigs[implementation].command; + + // Remove the flags that are not supported in versions before v0.4.0 + if (implementation === 'tapd' && isVersionBelow(version, '0.4.0-alpha')) { + command = command + .replace('--universe.public-access=rw', '--universe.public-access') + .replace('--universe.sync-all-assets', ''); + } + + return command; +}; + /** * Checks a range of port numbers to see if they are open on the current operating system. * Returns a new array of port numbers that are confirmed available diff --git a/src/utils/strings.spec.ts b/src/utils/strings.spec.ts index 6487b6727..d62285f4b 100644 --- a/src/utils/strings.spec.ts +++ b/src/utils/strings.spec.ts @@ -1,4 +1,4 @@ -import { ellipseInner, isVersionCompatible } from './strings'; +import { compareVersions, ellipseInner, isVersionCompatible } from './strings'; describe('strings util', () => { describe('ellipseInner', () => { @@ -36,6 +36,7 @@ describe('strings util', () => { expect(isVersionCompatible('0.17.0', '0.18.1')).toBe(true); expect(isVersionCompatible('0.17.2', '0.18.1')).toBe(true); expect(isVersionCompatible('0.18.0.1', '0.18.1')).toBe(true); + expect(isVersionCompatible('0.7.1-beta', '0.16.0-beta')).toBe(true); }); it('should return false for incompatible versions', () => { @@ -44,15 +45,55 @@ describe('strings util', () => { expect(isVersionCompatible('1.18.1', '0.18.1')).toBe(false); expect(isVersionCompatible('0.18.1.1', '0.18.1')).toBe(false); expect(isVersionCompatible('0.19.0.1', '0.18.1')).toBe(false); + expect(isVersionCompatible('0.17.1-beta', '0.6.0-beta')).toBe(false); }); - it('should return false for garbage input', () => { + it('should handle garbage input', () => { expect(isVersionCompatible('123', '0.18.1')).toBe(false); - expect(isVersionCompatible('asdf', '0.18.1')).toBe(false); - expect(isVersionCompatible('', '0.18.1')).toBe(false); - expect(isVersionCompatible('0.18.asds', '0.18.1')).toBe(false); - expect(isVersionCompatible('asf.18.0', '0.18.1')).toBe(false); - expect(isVersionCompatible(undefined as any, '0.18.1')).toBe(false); + expect(isVersionCompatible('asdf', 'xyz')).toBe(true); + expect(isVersionCompatible('', '0.18.1')).toBe(true); + expect(isVersionCompatible(undefined as any, '0.18.1')).toBe(true); + }); + }); + + describe('compareVersions', () => { + it('should return 0 for equal versions', () => { + expect(compareVersions('0.18.1', '0.18.1')).toBe(0); + expect(compareVersions('0.18.0', '0.18.0')).toBe(0); + expect(compareVersions('0.17.0', '0.17.0')).toBe(0); + expect(compareVersions('0.17.2', '0.17.2')).toBe(0); + }); + + it('should return 1 for higher versions', () => { + expect(compareVersions('0.19.0', '0.18.1')).toBe(1); + expect(compareVersions('0.18.2', '0.18.1')).toBe(1); + expect(compareVersions('1.18.1', '0.18.1')).toBe(1); + expect(compareVersions('1.18.1.1', '0.18.1')).toBe(1); + expect(compareVersions('0.17.1-beta.rc1', '0.17.1-beta')).toBe(1); + }); + + it('should return -1 for lower versions', () => { + expect(compareVersions('0.17.0', '0.18.1')).toBe(-1); + expect(compareVersions('0.7.2', '0.18.1')).toBe(-1); + expect(compareVersions('1.17.1', '1.18.1')).toBe(-1); + expect(compareVersions('1.17.1.1', '1.18.1')).toBe(-1); + expect(compareVersions('0.7.1-beta', '0.16.0-beta')).toBe(-1); + expect(compareVersions('0.17.1-beta', '0.17.1-beta.rc1')).toBe(-1); + }); + + it('should return 0 for garbage input', () => { + expect(compareVersions('asdf', 'xyz')).toBe(0); + expect(compareVersions('', '0.18.1')).toBe(0); + expect(compareVersions(undefined as any, '0.18.1')).toBe(0); + expect(compareVersions('0.18.1', undefined as any)).toBe(0); + }); + + it('should handle pre-release tags', () => { + expect(compareVersions('0.18.1-beta', '0.18.1-alpha')).toBe(0); + expect(compareVersions('0.18.1-beta', '0.18.1-beta')).toBe(0); + expect(compareVersions('0.18.1-beta', '0.18.1-beta.rc1')).toBe(-1); + expect(compareVersions('0.18.1-beta.rc2', '0.18.1-beta.rc1')).toBe(1); + expect(compareVersions('0.18.2-beta', '0.18.1-beta.rc1')).toBe(1); }); }); }); diff --git a/src/utils/strings.ts b/src/utils/strings.ts index bec7b95dd..1d257d77a 100644 --- a/src/utils/strings.ts +++ b/src/utils/strings.ts @@ -36,29 +36,47 @@ export const ellipseInner = ( * isVersionCompatible('0.19.0.1', '0.18.1') => false */ export const isVersionCompatible = (version: string, maxVersion: string): boolean => { + return compareVersions(version, maxVersion) <= 0; +}; + +/** + * Checks if the version provided is lower than the maximum version provided. + */ +export const isVersionBelow = (version: string, maxVersion: string): boolean => { + return compareVersions(version, maxVersion) < 0; +}; + +/** + * Compares two versions and returns a number indicating if the first version is higher, + * lower, or equal to the second version. + * @returns 0 if the versions are equal, 1 if the first version is higher, and -1 if the + * first version is lower + */ +export const compareVersions = (aVersion: string, bVersion: string): number => { // sanity checks - if (!version || !maxVersion) return false; + if (!aVersion || !bVersion) return 0; + + // helper function to split the version into an array of numbers using a regex + const split = (ver: string) => [...ver.matchAll(/\d+/g)].map(a => parseInt(a[0])); + // convert version into a number array - const versionParts = version.split('.').map(n => parseInt(n)); - // convert maxversion into a number array - const maxParts = maxVersion.split('.').map(n => parseInt(n)); + const aParts = split(aVersion); + // convert minVersion into a number array + const bParts = split(bVersion); + // get the longest length of the two versions. May be 3 or 4 with bitcoind - const len = Math.max(versionParts.length, maxParts.length); + const len = Math.max(aParts.length, bParts.length); // loop over each number in the version from left ot right for (let i = 0; i < len; i++) { - const ver = versionParts[i]; - const max = maxParts[i]; - // if version has more digits than maxVersion, return the result of the previous digit - // '0.18.0.1' <= '0.18.1' = true - // '0.18.1.1' <= '0.18.1' = false - if (max === undefined) return versionParts[i - 1] < maxParts[i - 1]; - // bail for non-numeric input - if (isNaN(ver) || isNaN(max)) return false; - // if any number is higher, then the version is not compatible - if (ver > max) return false; - // if the numder is lower, then return true immediately - if (ver < max) return true; - //if the digits are equal, check the next digit + const aNum = aParts[i] || 0; + const bNum = bParts[i] || 0; + // if the digit is higher, then return a positive number + if (aNum < bNum) return -1; + // if the digit is lower, then return a negative number + if (aNum > bNum) return 1; + // if the digits are equal, check the next digit } - return true; + + // if all digits are equal, return 0 + return 0; };