Skip to content
Draft
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
34 changes: 9 additions & 25 deletions dappmanager-grafana-dashboard.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,38 +195,22 @@
"mode": "fixed"
},
"decimals": 0,
"mappings": [
{
"options": {
"0": {
"color": "light-red",
"index": 0,
"text": "OFF"
},
"1": {
"color": "light-green",
"index": 1,
"text": "ON"
}
},
"type": "value"
}
],
"max": 1,
"mappings": [],
"max": 10,
"min": 0,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
"color": "red"
},
{
"color": "red",
"value": 80
"color": "green",
"value": 1
}
]
},
"unit": "bool_on_off"
"unit": "short"
},
"overrides": []
},
Expand Down Expand Up @@ -256,13 +240,13 @@
"uid": "PBFA97CFB590B2093"
},
"exemplar": true,
"expr": "dappmanager_ipfs_client_target_local{instance=\"dappmanager.dappnode:80\",ipfsClientTargetLocal=\"local\"}",
"expr": "dappmanager_ipfs_gateway_count{instance=\"dappmanager.dappnode:80\",gatewayCount=\"count\"}",
"interval": "",
"legendFormat": "Ipfs Client Local (on / off)",
"legendFormat": "IPFS Gateways Configured",
"refId": "A"
}
],
"title": "Ipfs Client Local",
"title": "IPFS Gateways",
"type": "gauge"
},
{
Expand Down
14 changes: 9 additions & 5 deletions packages/admin-ui/src/__mock-backend__/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IpfsClientTarget, PortProtocol, Routes } from "@dappnode/types";
import { PortProtocol, Routes } from "@dappnode/types";
import { autoUpdate } from "./autoUpdate";
import { devices } from "./devices";
import { fetchPkgsData } from "./fetchPkgsData";
Expand Down Expand Up @@ -221,10 +221,14 @@ export const otherCalls: Omit<Routes, keyof typeof namedSpacedCalls> = {
{ lv_name: "swap_1", vg_name: "rootvg", lv_size: "976.00m" }
],
lvmDiskSpaceExtend: async () => "Successfully extended LVM disk space",
ipfsClientTargetSet: async () => {},
ipfsClientTargetGet: async () => ({
ipfsClientTarget: IpfsClientTarget.remote,
ipfsGateway: "https://ipfs-gateway.dappnode.net"
ipfsGatewayUrlsSet: async () => {},
ipfsGatewayUrlsGet: async () => ({
ipfsGatewayUrls: [
"https://ipfs-gateway.dappnode.net",
"https://ipfs-gateway-dev.dappnode.net",
"https://gateway.pinata.cloud",
"https://cloudflare-ipfs.com"
]
}),
enableEthicalMetrics: async () => {},
getEthicalMetricsConfig: async () => ({
Expand Down
84 changes: 21 additions & 63 deletions packages/admin-ui/src/components/IpfsClient.tsx
Original file line number Diff line number Diff line change
@@ -1,80 +1,38 @@
import React from "react";
import "./multiClient.scss";
import { IpfsClientTarget } from "@dappnode/types";
import Card from "components/Card";
import { joinCssClass } from "utils/css";
import Input from "./Input";
import { IPFS_DAPPNODE_GATEWAY, IPFS_GATEWAY_CHECKER } from "params";
import RenderMarkdown from "./RenderMarkdown";

interface IpfsClientData {
title: string;
description: string;
option: IpfsClientTarget;
}
const description = `Configure the IPFS gateway URLs used for fetching Dappnode packages. The system will try your local IPFS node first, then race all configured gateways simultaneously - the first to respond wins.

Enter multiple URLs separated by commas. You can find public gateways at [${IPFS_GATEWAY_CHECKER}](${IPFS_GATEWAY_CHECKER}).

const clients: IpfsClientData[] = [
{
title: "Remote",
description: `Public IPFS node API mantained by Dappnode [${IPFS_DAPPNODE_GATEWAY}](${IPFS_DAPPNODE_GATEWAY}) or choose one from [${IPFS_GATEWAY_CHECKER}](${IPFS_GATEWAY_CHECKER})`,
option: IpfsClientTarget.remote
},
{
title: "Local",
description: "Your own IPFS node w/out 3rd parties",
option: IpfsClientTarget.local
}
];
Default gateway: [${IPFS_DAPPNODE_GATEWAY}](${IPFS_DAPPNODE_GATEWAY})`;

/**
* View to chose or change the IPFS client
* There are two main options:
* - Remote
* - Local
* Simple component to configure IPFS gateway URLs
*/
export function IpfsClient({
clientTarget: selectedClientTarget,
gatewayTarget,
onClientTargetChange,
onGatewayTargetChange
gatewayUrls,
onGatewayUrlsChange
}: {
clientTarget: IpfsClientTarget | null;
gatewayTarget: string | null;
onClientTargetChange: (newTarget: IpfsClientTarget) => void;
onGatewayTargetChange: (newTarget: string) => void;
/** Comma-separated gateway URLs string for display/editing */
gatewayUrls: string | null;
/** Callback receives comma-separated string of URLs */
onGatewayUrlsChange: (newUrls: string) => void;
}) {
return (
<div className="ipfs-multi-clients">
{clients
.filter(({ option }) => option.length > 0)
.map(({ title, description, option }) => {
const selected = selectedClientTarget && option === selectedClientTarget;

return (
<Card
key={option}
shadow
className={`ipfs-multi-client ${joinCssClass({ selected })}`}
onClick={() => {
// Prevent over-riding the options onClientTargetChange call
if (!selected) onClientTargetChange(option);
}}
>
<div className="title">{title}</div>
<div className="description">
<RenderMarkdown source={description} />
</div>
<div className="ipfs-gateway-config">
<div className="description" style={{ marginBottom: "1rem" }}>
<RenderMarkdown source={description} />
</div>

{option === "remote" && (
<Input
placeholder="https://ipfs-gateway.dappnode.net"
value={gatewayTarget || ""}
onValueChange={onGatewayTargetChange}
/>
)}
</Card>
);
})}
<label style={{ fontWeight: 500, marginBottom: "0.5rem", display: "block" }}>Gateway URLs</label>
<Input
placeholder="https://ipfs-gateway.dappnode.net, https://ipfs-gateway-dev.dappnode.net, https://gateway.pinata.cloud, https://cloudflare-ipfs.com"
value={gatewayUrls || ""}
onValueChange={onGatewayUrlsChange}
/>
</div>
);
}
112 changes: 68 additions & 44 deletions packages/admin-ui/src/pages/system/components/Ipfs/IpfsNode.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { useState, useEffect } from "react";
import { useApi, api } from "api";
import { IpfsClientTarget } from "@dappnode/types";
import { withToast } from "components/toast/Toast";
import { IpfsClient } from "components/IpfsClient";
import Button from "components/Button";
Expand All @@ -9,73 +8,98 @@ import { forumUrl } from "params";
import Card from "components/Card";
import SubTitle from "components/SubTitle";

/**
* Converts array of URLs to comma-separated string for display
*/
function urlsToString(urls: string[]): string {
return urls.join(", ");
}

/**
* Parses comma-separated string to array of URLs, trimming whitespace
*/
function stringToUrls(str: string): string[] {
return str
.split(",")
.map((url) => url.trim())
.filter((url) => url.length > 0);
}

export default function IpfsNode() {
const ipfsRepository = useApi.ipfsClientTargetGet();
const [ipfsClientTarget, setIpfsClientTarget] = useState<IpfsClientTarget | null>(null);
const [ipfsGatewayTarget, setIpfsGatewayTarget] = useState<string | null>(null);
const ipfsRepository = useApi.ipfsGatewayUrlsGet();
// Store as comma-separated string for easier editing
const [ipfsGatewayUrlsStr, setIpfsGatewayUrlsStr] = useState<string>("");

useEffect(() => {
if (ipfsRepository.data) setIpfsClientTarget(ipfsRepository.data.ipfsClientTarget);
if (ipfsRepository.data) setIpfsGatewayTarget(ipfsRepository.data.ipfsGateway);
if (ipfsRepository.data) {
setIpfsGatewayUrlsStr(urlsToString(ipfsRepository.data.ipfsGatewayUrls));
}
}, [ipfsRepository.data]);

async function changeIpfsClient() {
if (ipfsClientTarget && ipfsGatewayTarget)
async function saveGatewayUrls() {
if (ipfsGatewayUrlsStr) {
const gatewayUrls = stringToUrls(ipfsGatewayUrlsStr);
if (gatewayUrls.length === 0) {
// Don't allow empty gateway URLs
return;
}

await withToast(
() =>
api.ipfsClientTargetSet({
ipfsRepository: {
ipfsClientTarget: ipfsClientTarget,
ipfsGateway: ipfsGatewayTarget
}
api.ipfsGatewayUrlsSet({
ipfsGatewayUrls: gatewayUrls
}),
{
message: `Setting IPFS mode ${ipfsClientTarget}...`,
onSuccess: `Successfully changed to ${ipfsClientTarget}`
message: "Saving IPFS gateway URLs...",
onSuccess: "Successfully updated IPFS gateway URLs"
}
);
}
await ipfsRepository.revalidate();
}

/**
* Check if settings have changed from saved values
*/
function hasChanges(): boolean {
if (!ipfsRepository.data) return false;

const savedUrls = ipfsRepository.data.ipfsGatewayUrls;
const currentUrls = stringToUrls(ipfsGatewayUrlsStr);

return JSON.stringify(savedUrls) !== JSON.stringify(currentUrls);
}

return (
<div className="dappnode-identity">
<SubTitle>IPFS Node</SubTitle>
<SubTitle>IPFS Gateway Configuration</SubTitle>
<div className="section-spacing">
<Card>
<div>
Dappnode uses IPFS to distribute Dappnode packages in a decentralized way. Choose to connect to a remote
IPFS gateway or use your own local IPFS node.
Dappnode uses IPFS to distribute packages in a decentralized way. Configure your IPFS gateway URLs below.
</div>
<div>
More information at: <LinkDocs href={forumUrl.ipfsRemoteHowTo}>How to use Dappnode IPFS remote</LinkDocs>
<strong>Resilient fetching:</strong> The system will try your local IPFS node first (if available), then
race all configured gateways simultaneously - the first to respond wins.
</div>
</Card>
{ipfsRepository.data ? (
<>
<IpfsClient
clientTarget={ipfsClientTarget}
onClientTargetChange={setIpfsClientTarget}
gatewayTarget={ipfsGatewayTarget}
onGatewayTargetChange={setIpfsGatewayTarget}
/>

<br />
{ipfsRepository.data ? (
<>
<IpfsClient gatewayUrls={ipfsGatewayUrlsStr} onGatewayUrlsChange={setIpfsGatewayUrlsStr} />

<br />

<div style={{ textAlign: "end" }}>
<Button
variant="dappnode"
onClick={changeIpfsClient}
disabled={
!ipfsClientTarget ||
(ipfsRepository.data.ipfsClientTarget === ipfsClientTarget &&
ipfsRepository.data.ipfsGateway === ipfsGatewayTarget)
}
>
Change
</Button>
</div>
</>
) : null}
<div style={{ textAlign: "end" }}>
<Button variant="dappnode" onClick={saveGatewayUrls} disabled={!hasChanges()}>
Save
</Button>
</div>
</>
) : null}
<div>
More information at: <LinkDocs href={forumUrl.ipfsRemoteHowTo}>How to use Dappnode IPFS remote</LinkDocs>
</div>
</Card>
</div>
</div>
);
Expand Down
10 changes: 4 additions & 6 deletions packages/daemons/src/repositoryHealth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { logs } from "@dappnode/logger";
import { runAtMostEvery } from "@dappnode/utils";
import { notifications } from "@dappnode/notifications";
import { Category, Priority, Status } from "@dappnode/types";
import * as db from "@dappnode/db";
import { getIpfsUrl } from "@dappnode/installer";
import { params } from "@dappnode/params";
import { eventBus } from "@dappnode/eventbus";
Expand All @@ -15,7 +14,6 @@ let ethFailureCount = 0;
let ethNotificationSent = false;

async function checkIpfsHealth(): Promise<void> {
const ipfsClientTarget = db.ipfsClientTarget.get();
const ipfsUrl = getIpfsUrl();

const controller = new AbortController();
Expand All @@ -36,7 +34,7 @@ async function checkIpfsHealth(): Promise<void> {

if (!res.ok) throw new Error(`Status ${res.status}`);

logs.info(`IPFS endpoint (${ipfsClientTarget}) at ${ipfsUrl} is healthy`);
logs.info(`IPFS endpoint at ${ipfsUrl} is healthy`);

// reset failure count on success
ipfsFailureCount = 0;
Expand All @@ -56,20 +54,20 @@ async function checkIpfsHealth(): Promise<void> {
}
} catch (error) {
clearTimeout(timeout);
logs.error(`IPFS endpoint (${ipfsClientTarget}) at ${ipfsUrl} is unhealthy: ${error}`);
logs.error(`IPFS endpoint at ${ipfsUrl} is unhealthy: ${error}`);

// increment failure count and send notification after threshold
ipfsFailureCount += 1;
if (ipfsFailureCount >= 3 && !ipfsNotificationSent) {
await notifications.sendNotification({
title: "Your Dappnode IPFS endpoint is not resolving content correctly.",
dnpName: params.dappmanagerDnpName,
body: `Dappnode IPFS endpoint (${ipfsClientTarget}) at ${ipfsUrl} is currently unreachable or not resolving content correctly. This may affect access to decentralized content or applications relying on IPFS.`,
body: `Dappnode IPFS endpoint at ${ipfsUrl} is currently unreachable or not resolving content correctly. This may affect access to decentralized content or applications relying on IPFS.`,
category: Category.system,
priority: Priority.high,
status: Status.triggered,
callToAction: {
title: `Switch to ${ipfsClientTarget && ipfsClientTarget === "local" ? "Remote" : "Local"} IPFS`,
title: "Configure IPFS Gateways",
url: "http://my.dappnode/system/ipfs"
},
isBanner: true,
Expand Down
Loading
Loading