Skip to content

Commit

Permalink
Docker images config from GitHub (#532)
Browse files Browse the repository at this point in the history
* refactor saga to get docker image name from config

* implement image picker dropdown

* fix isCurrentOrNewerVersion

* refactor dropdown and add verified badge

* refactor getAvailableClientReleases
  • Loading branch information
BeroBurny authored Apr 9, 2021
1 parent 79d6b09 commit 1b6ed34
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {FormEvent, useRef, useState} from "react";
import React, {FormEvent, useEffect, useRef, useState} from "react";
import path from "path";
import {networksList} from "../../services/eth2/networks";
import {ButtonPrimary} from "../Button/ButtonStandard";
Expand All @@ -8,6 +8,7 @@ import {remote} from "electron";
import {getConfig} from "../../../config/config";
import {getDefaultsForClient} from "../../services/eth2/client/defaults";
import {Accordion} from "../Accordion/Accordion";
import {getAvailableClientReleases} from "../../services/utils/githubReleases";

export interface IConfigureBNSubmitOptions {
network: string;
Expand All @@ -18,6 +19,7 @@ export interface IConfigureBNSubmitOptions {
libp2pPort: string;
rpcPort: string;
memory: string;
image: string;
}

interface IConfigureBNProps {
Expand All @@ -33,6 +35,15 @@ export const ConfigureBeaconNode: React.FunctionComponent<IConfigureBNProps> = (
const [clientIndex, setClientIndex] = useState(0);
const defaults = getDefaultsForClient(props.clientName);

const [images, setImages] = useState([]);
useEffect(() => {
getAvailableClientReleases(clients[clientIndex]).then((imageList) => {
setImages(imageList);
setImageIndex(imageList.length - 1);
});
}, [clientIndex]);
const [imageIndex, setImageIndex] = useState(0);

const defaultChainDataDir = path.join(getConfig(remote.app).storage.dataDir, "beacon");
const [chainDataDir, setChainDataDir] = useState(defaultChainDataDir);

Expand Down Expand Up @@ -61,6 +72,7 @@ export const ConfigureBeaconNode: React.FunctionComponent<IConfigureBNProps> = (
memory,
network: networksList[networkIndex],
client: clients[clientIndex],
image: images[imageIndex],
});
};

Expand Down Expand Up @@ -117,6 +129,19 @@ export const ConfigureBeaconNode: React.FunctionComponent<IConfigureBNProps> = (
</div>

<Accordion label='Advanced' isOpen={showAdvanced} onClick={(): void => setShowAdvanced(!showAdvanced)}>
<div className='configure-port'>
<div className='row'>
<h3>Image</h3>
<p>(recommended: {images[images.length - 1]})</p>
</div>
<Dropdown
current={imageIndex}
onChange={setImageIndex}
options={images}
verifiedIndex={images.length - 1}
/>
</div>

<div className='configure-port'>
<div className='row'>
<h3>Chain data location</h3>
Expand Down
23 changes: 15 additions & 8 deletions src/renderer/components/Dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import * as React from "react";
import {useState} from "react";
import cgLogo from "../../assets/ico/app_icon.png";

export interface IDropdownProps {
options: Array<string> | {[id: number]: string};
current: number;
label?: string;
onChange?: (selected: number) => void;
verifiedIndex?: number;
}

export const Dropdown: React.FunctionComponent<IDropdownProps> = (props: IDropdownProps) => {
export const Dropdown: React.FunctionComponent<IDropdownProps> = ({
options,
current,
label,
onChange,
verifiedIndex,
}) => {
const [visible, setVisible] = useState("none");

const options = Array.isArray(props.options) ? {...props.options} : props.options;
options = Array.isArray(options) ? {...options} : options;

function showHide(): void {
visible === "none" ? setVisible("block") : setVisible("none");
Expand All @@ -22,7 +29,6 @@ export const Dropdown: React.FunctionComponent<IDropdownProps> = (props: IDropdo
}

function onOptionClick(key: number): void {
const {onChange} = props;
if (onChange) {
onChange(key);
showHide();
Expand All @@ -36,19 +42,20 @@ export const Dropdown: React.FunctionComponent<IDropdownProps> = (props: IDropdo
onClick={(): void => onOptionClick(key)}
className={`dropdown-item
${visible}
${key === props.current ? "selected" : ""}`}>
${key === current ? "selected" : ""}`}>
{options[key]}
{verifiedIndex === key && <img alt='ChainGuardian Logo' src={cgLogo} />}
</div>
);
}

return (
<div>
{props.label && <h3>{props.label}</h3>}
{label && <h3>{label}</h3>}
<div onClick={(): void => hide()} className='dropdown-screen'>
<div className='dropdown-container'>
<div onClick={(): void => props.onChange && showHide()} className='dropdown-selected'>
<div>{props.options[props.current]}</div>
<div onClick={(): void => onChange && showHide()} className='dropdown-selected'>
<div>{options[current]}</div>
</div>
<div className='dropdown-items-container'>
<div className='dropdown-items'>
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/components/Dropdown/dropdown.scss
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ z-index: 100;
&.selected{
background-color: $neutral-600;
}

img {
height: 18px;
margin-left: 10px;
margin-bottom: -3px;
}
}
.dropdown-item:first-child{
border-radius:8px 8px 0 0;
Expand Down
31 changes: 13 additions & 18 deletions src/renderer/ducks/beacon/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
select,
SelectEffect,
} from "redux-saga/effects";
import {getNetworkConfig} from "../../services/eth2/networks";
import {liveProcesses} from "../../services/utils/cmd";
import {
cancelDockerPull,
Expand Down Expand Up @@ -45,7 +44,7 @@ import {ValidatorBeaconNodes} from "../../models/validatorBeaconNodes";
import {createNotification} from "../notification/actions";
import {computeEpochAtSlot} from "@chainsafe/lodestar-beacon-state-transition";
import {ValidatorStatus} from "../../constants/validatorStatus";
import {cgLogger, createLogger, getBeaconLogfileFromURL} from "../../../main/logger";
import {cgLogger, createLogger, getBeaconLogfileFromURL, mainLogger} from "../../../main/logger";
import {setInitialBeacons} from "../settings/actions";
import {DockerRegistry} from "../../services/docker/docker-registry";
import {CgEth2ApiClient, readBeaconChainNetwork} from "../../services/eth2/client/module";
Expand All @@ -66,30 +65,25 @@ export function* pullDockerImage(

return effect !== undefined ? false : pullSuccess;
} catch (e) {
const message = e.stderr.includes("daemon is not running")
? "Seems Docker is offline, start it and try again"
: "Error while pulling Docker image, try again later";
yield put(createNotification({title: message, source: "pullDockerImage"}));
if (e.stderr) {
const message = e.stderr.includes("daemon is not running")
? "Seems Docker is offline, start it and try again"
: "Error while pulling Docker image, try again later";
yield put(createNotification({title: message, source: "pullDockerImage"}));
cgLogger.error(e.stderr);
} else {
yield put(createNotification({title: e.message, source: "pullDockerImage"}));
mainLogger.error(e.message);
}
yield put(endDockerImagePull());
cgLogger.error(e.stderr);
return false;
}
}

function* startLocalBeaconSaga({
payload: {network, client, chainDataDir, eth1Url, discoveryPort, libp2pPort, rpcPort, memory},
payload: {network, client, chainDataDir, eth1Url, discoveryPort, libp2pPort, rpcPort, memory, image},
meta: {onComplete},
}: ReturnType<typeof startLocalBeacon>): Generator<CallEffect | PutEffect, void, BeaconChain> {
const image = ((): string => {
switch (client) {
case "teku":
return process.env.DOCKER_TEKU_IMAGE;
case "lighthouse":
return process.env.DOCKER_LIGHTHOUSE_IMAGE;
default:
return getNetworkConfig(network).dockerConfig.image;
}
})();
const pullSuccess = yield call(pullDockerImage, image);

const ports = [
Expand All @@ -116,6 +110,7 @@ function* startLocalBeaconSaga({
memory,
eth1Url,
chainDataDir,
image,
}),
)).getParams().name,
network,
Expand Down
39 changes: 24 additions & 15 deletions src/renderer/services/eth2/client/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ export interface IDefaultBeaconNodeConfig {
libp2pPort: number;
discoveryPort: number;
memory: string;
owner: string;
repo: string;
dockerImage: string;
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
Expand All @@ -18,21 +21,24 @@ export interface IAllDefaults {
}

export const getAllDefaults = (): IAllDefaults => {
const context = require.context(".", true, /\/(.*)\/(.*)defaults.ts/);
return context
.keys()
.map((path) => ({
name: path.split("/")[1],
beacon: context(path).beaconNode,
validator: context(path).validator,
}))
.reduce(
(prev, {name, beacon, validator}) => ({
...prev,
[name]: {beacon, validator},
}),
{},
);
if (process.env.NODE_ENV !== "test") {
const context = require.context(".", true, /\/(.*)\/(.*)defaults.ts/);
return context
.keys()
.map((path) => ({
name: path.split("/")[1],
beacon: context(path).beaconNode,
validator: context(path).validator,
}))
.reduce(
(prev, {name, beacon, validator}) => ({
...prev,
[name]: {beacon, validator},
}),
{},
);
}
return {};
};

const defaults = getAllDefaults();
Expand All @@ -44,6 +50,9 @@ export const getDefaultsForClient = (clientName: string): IDefaults =>
libp2pPort: 9000,
discoveryPort: 9000,
memory: "3500m",
owner: "none",
repo: "none",
dockerImage: "none",
},
validator: {},
};
3 changes: 3 additions & 0 deletions src/renderer/services/eth2/client/lighthouse/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export const beaconNode: IDefaultBeaconNodeConfig = {
libp2pPort: 9000,
discoveryPort: 9000,
memory: "3500m",
owner: "sigp",
repo: "lighthouse",
dockerImage: process.env.DOCKER_LIGHTHOUSE_IMAGE,
};

export const validator: IDefaultValidatorConfig = {};
3 changes: 3 additions & 0 deletions src/renderer/services/eth2/client/teku/defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export const beaconNode: IDefaultBeaconNodeConfig = {
libp2pPort: 9000,
discoveryPort: 9000,
memory: "3500m",
owner: "ConsenSys",
repo: "teku",
dockerImage: process.env.DOCKER_TEKU_IMAGE,
};

export const validator: IDefaultValidatorConfig = {};
46 changes: 46 additions & 0 deletions src/renderer/services/utils/githubReleases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import axios, {AxiosResponse} from "axios";
import {IGithubRelease} from "../../../../types/githubRelease.dto";
import {getDefaultsForClient} from "../eth2/client/defaults";

const gitHubReposInstance = axios.create({
baseURL: "https://api.github.com/repos/",
timeout: 1000,
});

export const getReleases = (owner: string, repo: string): Promise<AxiosResponse<IGithubRelease[]>> =>
gitHubReposInstance.get<IGithubRelease[]>(`/${owner}/${repo}/releases`);

export const isCurrentOrNewerVersion = (current: string, comparingWith: string): boolean => {
if (current === comparingWith) return true;

const currentFragments = current.replace(/[^\d.-]/g, "").split(".");
const comparingWithFragments = comparingWith.replace(/[^\d.-]/g, "").split(".");

const length =
currentFragments.length > comparingWithFragments.length
? currentFragments.length
: comparingWithFragments.length;
for (let i = 0; i < length; i++) {
if ((Number(currentFragments[i]) || 0) === (Number(comparingWithFragments[i]) || 0)) continue;
return (Number(comparingWithFragments[i]) || 0) > (Number(currentFragments[i]) || 0);
}
return true;
};

export const getAvailableClientReleases = async (client: string): Promise<string[]> => {
const {
beacon: {dockerImage, owner, repo},
} = getDefaultsForClient(client);
const response = await getReleases(owner, repo);
const currentTag = dockerImage.split(":")[1];
return (
response.data
.filter(
// eslint-disable-next-line camelcase, @typescript-eslint/camelcase
({tag_name, draft, prerelease}) =>
!draft && !prerelease && isCurrentOrNewerVersion(currentTag, tag_name),
)
// eslint-disable-next-line camelcase, @typescript-eslint/camelcase
.map(({tag_name}) => dockerImage.split(":")[0] + ":" + tag_name)
);
};
33 changes: 33 additions & 0 deletions test/unit/utils/githubReleases.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {isCurrentOrNewerVersion} from "../../../src/renderer/services/utils/githubReleases";

describe("GitHub Releases", () => {
describe("isCurrentOrNewerVersion", () => {
it("test with 'v' prefix", () => {
expect(isCurrentOrNewerVersion("v1.3.2", "v1.3.2")).toBeTruthy();
expect(isCurrentOrNewerVersion("v1.3.2", "v1.3.2")).toBeTruthy();
expect(isCurrentOrNewerVersion("v1.3.2", "v2.3.2")).toBeTruthy();
expect(isCurrentOrNewerVersion("v1.3.2", "v2.1.2")).toBeTruthy();
expect(isCurrentOrNewerVersion("v1.3", "v1.3.0")).toBeTruthy();
expect(isCurrentOrNewerVersion("v1.3", "v1.3.3")).toBeTruthy();

expect(isCurrentOrNewerVersion("v1.3.2", "v1.3.1")).toBeFalsy();
expect(isCurrentOrNewerVersion("v1.4.2", "v1.3.11")).toBeFalsy();
expect(isCurrentOrNewerVersion("v1.3.2", "v1.3")).toBeFalsy();
expect(isCurrentOrNewerVersion("v1.3.2", "v0.2.1")).toBeFalsy();
});

it("test without v prefix", () => {
expect(isCurrentOrNewerVersion("1.3.2", "1.3.2")).toBeTruthy();
expect(isCurrentOrNewerVersion("1.3.2", "1.3.2")).toBeTruthy();
expect(isCurrentOrNewerVersion("1.3.2", "2.3.2")).toBeTruthy();
expect(isCurrentOrNewerVersion("1.3.2", "2.1.2")).toBeTruthy();
expect(isCurrentOrNewerVersion("1.3", "1.3.0")).toBeTruthy();
expect(isCurrentOrNewerVersion("1.3", "1.3.3")).toBeTruthy();

expect(isCurrentOrNewerVersion("1.3.2", "1.3.1")).toBeFalsy();
expect(isCurrentOrNewerVersion("1.4.2", "1.3.11")).toBeFalsy();
expect(isCurrentOrNewerVersion("1.3.2", "1.3")).toBeFalsy();
expect(isCurrentOrNewerVersion("1.3.2", "0.2.1")).toBeFalsy();
});
});
});
Loading

0 comments on commit 1b6ed34

Please sign in to comment.