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

Feature: Add ArgoCD widget #4305

Merged
merged 8 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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
33 changes: 33 additions & 0 deletions docs/widgets/services/argocd.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
title: ArgoCD
description: ArgoCD Widget Configuration
---

Learn more about [ArgoCD](https://argo-cd.readthedocs.io/en/stable/).

Allowed fields (limited to a max of 4): `["apps", "synced", "outOfSync", "healthy", "progressing", "degraded", "suspended", "missing"]`

```yaml
widget:
type: argocd
url: http://argocd.host.or.ip:port
key: argocdapikey
```

You can generate an API key either by creating a bearer token for an existing account, see [Authorization](https://argo-cd.readthedocs.io/en/latest/developer-guide/api-docs/#authorization) (not recommended) or create a new local user account with limited privileges and generate an authentication token for this account. To do this the steps are:

- [Create a new local user](https://argo-cd.readthedocs.io/en/stable/operator-manual/user-management/#create-new-user) and give it the `apiKey` capability
- Setup [RBAC configuration](https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/#rbac-configuration) for your the user and give it readonly access to your ArgoCD resources, e.g. by giving it the `role:readonly` role.
- In your ArgoCD project under _Settings / Accounts_ open the newly created account and in the _Tokens_ section click on _Generate New_ to generate an access token, optionally specifying an expiry date.

If you installed ArgoCD via the official Helm chart, the account creation and rbac config can be achived by overriding these helm values:

```yaml
configs:
cm:
accounts.readonly: apiKey
rbac:
policy.csv: "g, readonly, role:readonly"
```

This creates a new account called `readonly` and attaches the `role:readonly` role to it.
1 change: 1 addition & 0 deletions docs/widgets/services/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ search:
You can also find a list of all available service widgets in the sidebar navigation.

- [Adguard Home](adguard-home.md)
- [ArgoCD](argocd.md)
- [Atsumeru](atsumeru.md)
- [Audiobookshelf](audiobookshelf.md)
- [Authentik](authentik.md)
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ nav:
- "Service Widgets":
- widgets/services/index.md
- widgets/services/adguard-home.md
- widgets/services/argocd.md
- widgets/services/atsumeru.md
- widgets/services/audiobookshelf.md
- widgets/services/authentik.md
Expand Down
10 changes: 10 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -988,5 +988,15 @@
"memory": "MEM",
"disk": "Disk",
"network": "NET"
},
"argocd": {
"apps": "Apps",
"synced": "Synced",
"outOfSync": "OutOfSync",
fcornelius marked this conversation as resolved.
Show resolved Hide resolved
"healthy": "Healthy",
"degraded": "Degraded",
"progressing": "Progressing",
"missing": "Missing",
"suspended": "Suspended"
}
}
1 change: 1 addition & 0 deletions src/utils/proxy/handlers/credentialed.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export default async function credentialedProxyHandler(req, res, map) {
headers["X-gotify-Key"] = `${widget.key}`;
} else if (
[
"argocd",
"authentik",
"cloudflared",
"ghostfolio",
Expand Down
52 changes: 52 additions & 0 deletions src/widgets/argocd/component.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";

export default function Component({ service }) {
const { widget } = service;

// Limits fields to available statuses
const validFields = ["apps", "synced", "outOfSync", "healthy", "progressing", "degraded", "suspended", "missing"];
widget.fields = widget.fields?.filter((field) => validFields.includes(field));

// Limits max number of displayed fields
fcornelius marked this conversation as resolved.
Show resolved Hide resolved
const MAX_ALLOWED_FIELDS = 4;
if (widget.fields?.length > MAX_ALLOWED_FIELDS) {
widget.fields = widget.fields.slice(0, MAX_ALLOWED_FIELDS);
}

const { data: appsData, error: appsError } = useWidgetAPI(widget, "applications");

const appCounts = widget.fields?.map((status) => {
if (status === "apps") {
return { status, count: appsData?.items?.length };
}
const apiStatus = status.charAt(0).toUpperCase() + status.slice(1);
const count = appsData?.items?.filter(
(item) => item.status?.sync?.status === apiStatus || item.status?.health?.status === apiStatus,
fcornelius marked this conversation as resolved.
Show resolved Hide resolved
).length;
return { status, count };
});

if (appsError) {
return <Container service={service} error={appsError} />;
}

if (!appsData) {
return (
<Container service={service}>
{appCounts?.map((a) => (
<Block label={`argocd.${a.status}`} key={a.status} />
))}
</Container>
);
}

return (
<Container service={service}>
{appCounts?.map((a) => (
<Block label={`argocd.${a.status}`} key={a.status} value={a.count} />
))}
</Container>
);
}
14 changes: 14 additions & 0 deletions src/widgets/argocd/widget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import credentialedProxyHandler from "utils/proxy/handlers/credentialed";

const widget = {
api: "{url}/api/v1/{endpoint}",
proxyHandler: credentialedProxyHandler,

mappings: {
applications: {
endpoint: "applications",
},
},
};

export default widget;
1 change: 1 addition & 0 deletions src/widgets/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import dynamic from "next/dynamic";

const components = {
adguard: dynamic(() => import("./adguard/component")),
argocd: dynamic(() => import("./argocd/component")),
atsumeru: dynamic(() => import("./atsumeru/component")),
audiobookshelf: dynamic(() => import("./audiobookshelf/component")),
authentik: dynamic(() => import("./authentik/component")),
Expand Down
1 change: 1 addition & 0 deletions src/widgets/prometheusmetric/component.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Block from "components/services/widget/block";
import useWidgetAPI from "utils/proxy/use-widget-api";

function formatValue(t, metric, rawValue) {
if (!metric?.format) return rawValue;
shamoon marked this conversation as resolved.
Show resolved Hide resolved
if (!rawValue) return "-";

let value = rawValue;
Expand Down
2 changes: 2 additions & 0 deletions src/widgets/widgets.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import adguard from "./adguard/widget";
import argocd from "./argocd/widget";
import atsumeru from "./atsumeru/widget";
import audiobookshelf from "./audiobookshelf/widget";
import authentik from "./authentik/widget";
Expand Down Expand Up @@ -130,6 +131,7 @@ import zabbix from "./zabbix/widget";

const widgets = {
adguard,
argocd,
atsumeru,
audiobookshelf,
authentik,
Expand Down