Add dashboard support for externally hosted Grafana (#7596)

Fixes #7429

Currently Linkerd assumes that the Grafana instance is hosted
on-cluster. Some users would like to use externally hosted Grafana
instances, such as Grafana Cloud or AWS Managed Grafana.

In general users will have multiple Linkerd clusters with dashboards
in the same Grafana workspace, so we need to introduce a prefix for the
Grafana dashboard UID's so that they remain unique. 

This PR adds two new viz config values, `grafana.uidPrefix` and
`grafana.externalUrl`.
When grafana.uidPrefix is set, it will insert the user-supplied prefix
in the URL's for the Grafana dashboards. When grafana.externalUrl is
set, its value will be used for the links to Grafana dashboards instead
of using the grafana reverse proxy.

Signed-off-by: Jack Gill <jack.gill@elationhealth.com>
This commit is contained in:
Jack Gill 2022-01-19 06:30:24 -07:00 committed by GitHub
parent 85a4138442
commit 6c3da0279d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 119 additions and 21 deletions

View File

@ -96,7 +96,9 @@ Kubernetes: `>=1.20.0-0`
| defaultUID | int | `2103` | UID for all the viz components |
| enablePSP | bool | `false` | Create Roles and RoleBindings to associate this extension's ServiceAccounts to the control plane PSP resource. This requires that `enabledPSP` is set to true on the control plane install. Note PSP has been deprecated since k8s v1.21 |
| enablePodAntiAffinity | bool | `false` | Enables Pod Anti Affinity logic to balance the placement of replicas across hosts and zones for High Availability. Enable this only when you have multiple replicas of components. |
| grafana.url | string | `nil` | url of a Grafana instance with reverse proxy configured, used by the Linkerd viz web dashboard to provide direct links to specific Grafana dashboards. See the [Linkerd documentation](https://linkerd.io/2/tasks/grafana) for more information |
| grafana.externalUrl | string | `nil` | url of a Grafana instance hosted off-cluster. Cannot be set if grafana.url is set. The reverse proxy will not be used for this URL. |
| grafana.uidPrefix | string | `nil` | prefix for Grafana dashboard UID's, used when grafana.externalUrl is set. |
| grafana.url | string | `nil` | url of an in-cluster Grafana instance with reverse proxy configured, used by the Linkerd viz web dashboard to provide direct links to specific Grafana dashboards. Cannot be set if grafana.externalUrl is set. See the [Linkerd documentation](https://linkerd.io/2/tasks/grafana) for more information |
| identityTrustDomain | string | clusterDomain | Trust domain used for identity |
| imagePullSecrets | list | `[]` | For Private docker registries, authentication is needed. Registry secrets are applied to the respective service accounts |
| jaegerUrl | string | `""` | url of external jaeger instance Set this to `jaeger.linkerd-jaeger.svc.<clusterDomain>` if you plan to use jaeger extension |

View File

@ -73,9 +73,18 @@ spec:
- args:
- -linkerd-metrics-api-addr=metrics-api.{{.Release.Namespace}}.svc.{{.Values.clusterDomain}}:8085
- -cluster-domain={{.Values.clusterDomain}}
{{- if and .Values.grafana.url .Values.grafana.externalUrl }}
{{ fail "Cannot set both grafana.url (on-cluster Grafana) and grafana.externalUrl (off-cluster Grafana)"}}
{{- end}}
{{- if .Values.grafana.url }}
- -grafana-addr={{.Values.grafana.url}}
{{- end}}
{{- if .Values.grafana.externalUrl }}
- -grafana-external-addr={{.Values.grafana.externalUrl}}
{{- end}}
{{- if .Values.grafana.uidPrefix }}
- -grafana-prefix={{.Values.grafana.uidPrefix}}
{{- end}}
{{- if .Values.jaegerUrl }}
- -jaeger-addr={{.Values.jaegerUrl}}
{{- end}}

View File

@ -344,11 +344,17 @@ dashboard:
# resources:
grafana:
# -- url of a Grafana instance with reverse proxy configured, used by the
# -- url of an in-cluster Grafana instance with reverse proxy configured, used by the
# Linkerd viz web dashboard to provide direct links to specific Grafana
# dashboards. See the [Linkerd
# dashboards. Cannot be set if grafana.externalUrl is set. See the [Linkerd
# documentation](https://linkerd.io/2/tasks/grafana) for more information
url:
# -- url of a Grafana instance hosted off-cluster. Cannot be set if
# grafana.url is set. The reverse proxy will not be used for this URL.
externalUrl:
# -- prefix for Grafana dashboard UID's, used when grafana.externalUrl is
# set.
uidPrefix:
prometheus:
# -- toggle field to enable or disable prometheus

View File

@ -3,19 +3,49 @@ import React from 'react';
import _isEmpty from 'lodash/isEmpty';
import { grafanaIcon } from './util/SvgWrappers.jsx';
const GrafanaLink = function({ PrefixedLink, name, namespace, resource }) {
let link = `/grafana/d/linkerd-${resource}?var-${resource}=${name}`;
const GrafanaLink = function({ PrefixedLink, name, namespace, resource, grafanaExternalUrl, grafanaPrefix }) {
let link = '/grafana/d/';
if (grafanaExternalUrl !== '') {
let baseUrl = grafanaExternalUrl;
// strip trailing slash if present, to avoid double slash in final URL
if (grafanaExternalUrl.charAt(grafanaExternalUrl.length - 1) === '/') {
baseUrl = grafanaExternalUrl.slice(0, grafanaExternalUrl.length - 1);
}
// grafanaPrefix is used for externally hosted Grafana instances, which do not use the /grafana proxy.
// When dashboards for multiple Linkerd instances are deployed to the same Grafana host,
// the dashboard UID needs a user-specified prefix to be unique.
link = `${baseUrl}/d/${grafanaPrefix}`;
}
link += `linkerd-${resource}?var-${resource}=${name}`;
if (!_isEmpty(namespace)) {
link += `&var-namespace=${namespace}`;
}
return (
<PrefixedLink
to={link}
targetBlank>
&nbsp;&nbsp;
{grafanaIcon}
</PrefixedLink>
);
if (grafanaExternalUrl !== '') {
return (
// <a> instead of <PrefixedLink> because <Link> doesn't work with external URL's
<a
href={link}
rel="noreferrer"
target="_blank">
&nbsp;&nbsp;
{grafanaIcon}
</a>
);
} else {
return (
<PrefixedLink
to={link}
targetBlank>
&nbsp;&nbsp;
{grafanaIcon}
</PrefixedLink>
);
}
};
GrafanaLink.propTypes = {
@ -23,10 +53,14 @@ GrafanaLink.propTypes = {
namespace: PropTypes.string,
PrefixedLink: PropTypes.func.isRequired,
resource: PropTypes.string.isRequired,
grafanaExternalUrl: PropTypes.string,
grafanaPrefix: PropTypes.string,
};
GrafanaLink.defaultProps = {
namespace: '',
grafanaExternalUrl: '',
grafanaPrefix: '',
};
export default GrafanaLink;

View File

@ -47,4 +47,25 @@ describe('GrafanaLink', () => {
expect(href).not.toContain("namespace");
expect(href).toContain(expectedVarNameStr);
});
it('Use grafana.externalUrl', () => {
let grafanaPrefix = "test-prefix";
let grafanaExternalUrl = "https://example.com";
let api = ApiHelpers('');
let linkProps = {
resource: "replicationcontroller",
name: "aldksf-3409823049823",
namespace: "myns",
grafanaPrefix: grafanaPrefix,
grafanaExternalUrl: grafanaExternalUrl,
};
let component = mount(routerWrap(GrafanaLink, linkProps));
expect(component.find("GrafanaLink")).toHaveLength(1);
const href = component.find('a').props().href;
expect(href).toContain(grafanaPrefix);
expect(href).toContain(grafanaExternalUrl);
});
});

View File

@ -187,7 +187,7 @@ const gatewayColumns = [
},
];
const columnDefinitions = (resource, showNamespaceColumn, showNameColumn, PrefixedLink, isTcpTable, hasTsStats, grafana, jaeger) => {
const columnDefinitions = (resource, showNamespaceColumn, showNameColumn, PrefixedLink, isTcpTable, hasTsStats, grafana, grafanaExternalUrl, grafanaPrefix, jaeger) => {
const isAuthorityTable = resource === 'authority';
const isTrafficSplitTable = resource === 'trafficsplit';
const isServicesTable = resource === 'service';
@ -228,6 +228,8 @@ const columnDefinitions = (resource, showNamespaceColumn, showNameColumn, Prefix
name={row.name}
namespace={row.namespace}
resource={row.type}
grafanaExternalUrl={grafanaExternalUrl}
grafanaPrefix={grafanaPrefix}
PrefixedLink={PrefixedLink} />
);
},
@ -304,7 +306,7 @@ const columnDefinitions = (resource, showNamespaceColumn, showNameColumn, Prefix
}
if (!isTrafficSplitTable) {
if (grafana !== '') {
if (grafana !== '' || grafanaExternalUrl !== '') {
columns = columns.concat(grafanaColumn);
}
if (jaeger !== '') {
@ -331,13 +333,13 @@ const preprocessMetrics = metrics => {
return tableData;
};
const MetricsTable = function({ metrics, resource, showNamespaceColumn, showName, title, api, isTcpTable, selectedNamespace, grafana, jaeger }) {
const MetricsTable = function({ metrics, resource, showNamespaceColumn, showName, title, api, isTcpTable, selectedNamespace, grafana, grafanaExternalUrl, grafanaPrefix, jaeger }) {
const showNsColumn = resource === 'namespace' || selectedNamespace !== '_all' ? false : showNamespaceColumn;
const showNameColumn = resource !== 'trafficsplit' ? true : showName;
let orderBy = 'name';
if (resource === 'trafficsplit' && !showNameColumn) { orderBy = 'leaf'; }
const hasTsStats = _some(metrics, m => m.tsStats);
const columns = columnDefinitions(resource, showNsColumn, showNameColumn, api.PrefixedLink, isTcpTable, hasTsStats, grafana, jaeger);
const columns = columnDefinitions(resource, showNsColumn, showNameColumn, api.PrefixedLink, isTcpTable, hasTsStats, grafana, grafanaExternalUrl, grafanaPrefix, jaeger);
const rows = preprocessMetrics(metrics);
return (
<BaseTable
@ -363,6 +365,8 @@ MetricsTable.propTypes = {
showNamespaceColumn: PropTypes.bool,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
grafana: PropTypes.string,
grafanaExternalUrl: PropTypes.string,
grafanaPrefix: PropTypes.string,
jaeger: PropTypes.string,
};
@ -371,6 +375,8 @@ MetricsTable.defaultProps = {
showName: true,
title: '',
grafana: '',
grafanaExternalUrl: '',
grafanaPrefix: '',
jaeger: '',
isTcpTable: false,
metrics: [],

View File

@ -30,6 +30,8 @@ func main() {
metricsAddr := cmd.String("metrics-addr", ":9994", "address to serve scrapable metrics on")
vizAPIAddr := cmd.String("linkerd-metrics-api-addr", "127.0.0.1:8085", "address of the linkerd-metrics-api service")
grafanaAddr := cmd.String("grafana-addr", "", "address of the linkerd-grafana service")
grafanaExternalAddr := cmd.String("grafana-external-addr", "", "address of the external grafana service")
grafanaPrefix := cmd.String("grafana-prefix", "", "prefix for Grafana dashboard UID's")
jaegerAddr := cmd.String("jaeger-addr", "", "address of the jaeger service")
templateDir := cmd.String("template-dir", "templates", "directory to search for template files")
staticDir := cmd.String("static-dir", "app/dist", "directory to search for static files")
@ -94,7 +96,7 @@ func main() {
log.Fatalf("invalid --enforced-host parameter: %s", err)
}
server := srv.NewServer(*addr, *grafanaAddr, *jaegerAddr, *templateDir, *staticDir, uuid, version,
server := srv.NewServer(*addr, *grafanaAddr, *grafanaExternalAddr, *grafanaPrefix, *jaegerAddr, *templateDir, *staticDir, uuid, version,
*controllerNamespace, *clusterDomain, *reload, reHost, client, k8sAPI, hc)
go func() {

View File

@ -28,6 +28,8 @@ type (
controllerNamespace string
clusterDomain string
grafana string
grafanaExternalURL string
grafanaPrefix string
jaeger string
grafanaProxy *reverseProxy
jaegerProxy *reverseProxy
@ -49,6 +51,8 @@ func (h *handler) handleIndex(w http.ResponseWriter, req *http.Request, p httpro
ControllerNamespace: h.controllerNamespace,
PathPrefix: pathPfx,
Grafana: h.grafana,
GrafanaExternalURL: h.grafanaExternalURL,
GrafanaPrefix: h.grafanaPrefix,
Jaeger: h.jaeger,
}

View File

@ -53,6 +53,8 @@ type (
PathPrefix string
Jaeger string
Grafana string
GrafanaExternalURL string
GrafanaPrefix string
}
healthChecker interface {
@ -83,6 +85,8 @@ Please see https://linkerd.io/dns-rebinding for an explanation of what is happen
func NewServer(
addr string,
grafanaAddr string,
grafanaExternalAddr string,
grafanaPrefix string,
jaegerAddr string,
templateDir string,
staticDir string,
@ -117,14 +121,20 @@ func NewServer(
version: version,
controllerNamespace: controllerNamespace,
clusterDomain: clusterDomain,
grafanaProxy: newReverseProxy(grafanaAddr, "/grafana"),
jaegerProxy: newReverseProxy(jaegerAddr, ""),
grafana: grafanaAddr,
grafanaExternalURL: grafanaExternalAddr,
grafanaPrefix: grafanaPrefix,
jaeger: jaegerAddr,
hc: hc,
statCache: cache.New(statExpiration, statCleanupInterval),
}
// Only create the grafana reverse proxy if we aren't using external grafana
if grafanaExternalAddr == "" {
handler.grafanaProxy = newReverseProxy(grafanaAddr, "/grafana")
}
httpServer := &http.Server{
Addr: addr,
ReadTimeout: timeout,
@ -201,8 +211,10 @@ func NewServer(
server.router.GET("/api/gateways", handler.handleAPIGateways)
server.router.GET("/api/extensions", handler.handleGetExtensions)
// grafana proxy
server.handleAllOperationsForPath("/grafana/*grafanapath", handler.handleGrafana)
// grafana proxy, only used if external grafana is not in use
if grafanaExternalAddr == "" {
server.handleAllOperationsForPath("/grafana/*grafanapath", handler.handleGrafana)
}
// jaeger proxy
server.handleAllOperationsForPath("/jaeger/*jaegerpath", handler.handleJaeger)

View File

@ -4,6 +4,8 @@
data-controller-namespace="{{.ControllerNamespace}}"
data-uuid="{{.UUID}}"
data-grafana="{{.Grafana}}"
data-grafana-external-url="{{.GrafanaExternalURL}}"
data-grafana-prefix="{{.GrafanaPrefix}}"
data-jaeger="{{.Jaeger}}">
{{ if .Error }}
<p>Failed to call public API: {{ .ErrorMessage }}</p>