Skip to content

Latest commit

 

History

History
612 lines (485 loc) · 27.2 KB

configuration.md

File metadata and controls

612 lines (485 loc) · 27.2 KB

Configuration

What the browser receives

The portal ships two tiers of configuration to the browser.

The public config is the <script type="backstage.io/config"> block of the unauthenticated index.html: every path a config.d.ts marks @visibility frontend, served to anyone who can reach the portal, signed in or not. It is kept to what the sign-in page needs before anyone is signed in:

Path Why it is public
app.* (title, baseUrl, release version, portals, extensions, routes, branding, Sentry, TelemetryDeck) the app shell and the sign-in page render from it
backend.baseUrl where the sign-in flow and every request go
auth.environment, auth.providers.* which providers exist (their secrets are @visibility secret)
gs.authProvider the provider the sign-in page initiates
gs.auth.scopes, gs.auth.extraScopes the scopes the sign-in requests
gs.signInProvider.*, gs.signInFallbackProvider.* the two sign-in cards
gs.github.brokerAudience picks the GitHub auth API when the app constructs its APIs, before sign-in; an audience name
organization.name, permission.enabled Backstage core

The signed-in config is served by the authenticated GET /api/gs/config, once, after the main sign-in, in app-config shape. Everything else a Giant Swarm plugin reads in the browser comes from here: the installations map (with the customer-identifying base domains), gs.adminGroups, the cluster token broker URL, the link templates of the cluster, deployment and home pages, the friendly labels and annotations, the Kubernetes end-of-life table, the proxy tuning knobs, the muster installations, the MCP server list and the chat's welcome copy, the skill repositories and the Flux Git host patterns. The complete list is SIGNED_IN_CONFIG_PATHS in the gs-backend plugin; the frontend reads it through @giantswarm/backstage-plugin-gs-react (useSignedInConfig() in components, getSignedInConfig() in the utility APIs constructed at app boot).

To let the browser read a new key: leave its config.d.ts entry at the default (backend) visibility, add the path to SIGNED_IN_CONFIG_PATHS and read it through the signed-in config. @visibility frontend is only for what the sign-in page itself needs, per field: @deepVisibility frontend is not used, because it would ship every field later added under that key to every visitor, and a test refuses it in every config.d.ts of this repository. A public field is validated when the app-backend starts, and a missing required one stops the start, so a public leaf a deployment fills from the environment (the loader drops a key whose variable is unset) is declared optional.

The public set is pinned: packages/app/src/config/frontendVisiblePaths.golden.json lists every frontend-visible path of the app's merged config schema, and frontendVisibility.test.ts (part of ci:verify) fails when the schema exposes anything else, whether from a new field, a new plugin or a dependency bump. Regenerate it, and review the diff, with UPDATE_GOLDEN=1 yarn workspace app test src/config.

Cluster access (broker-only)

Backstage reaches every management cluster through a single main Dex login. The muster cluster-token broker exchanges the user's main session for a per-cluster Kubernetes token (RFC 8693), so covered clusters need no per-cluster OIDC login popup and no auth.providers.oidc-<mc> block. A complete, annotated example is in /app-config.example.yaml.

Main login provider

gs:
  # Name of the single main login provider (defined under auth.providers).
  authProvider: oidc-main

gs.clusterTokenBroker

Setting tokenUrl (muster) or at least one entry under targets (an installation's own Dex, see below) enables the silent per-cluster token path. Without either, Backstage falls back to per-cluster OIDC popups.

gs:
  clusterTokenBroker:
    # OAuth token endpoint of the broker. Its presence enables the broker path.
    tokenUrl: https://muster.example.gigantic.io/oauth/token
    # Confidential client registered with the broker (backend-only).
    clientId: ${CLUSTER_TOKEN_BROKER_CLIENT_ID}
    clientSecret: ${CLUSTER_TOKEN_BROKER_CLIENT_SECRET}
    # Optional. Usually unset -- the broker's per-audience config owns the scope.
    # scope: ...

Without muster: a Dex target per installation

A portal whose management cluster runs no muster exchanges the main session at each other installation's own Dex instead. Dex implements the RFC 8693 grant with its connector_id extension: the installation's Dex needs an OIDC connector whose issuer is the portal's main Dex (getUserInfo: false, and the CA of that issuer in rootCAs when it is not publicly trusted) and a confidential client for the portal, listed in the apiserver client's trustedPeers so the cross-client scope below is granted. The issued token is an id_token of the installation's Dex with the apiserver's client as its audience, the same token a per-cluster login would have given.

gs:
  clusterTokenBroker:
    # tokenUrl, clientId and clientSecret (muster) are optional once a target
    # exists; with both, the targets win for their installations and muster
    # serves the rest.
    targets:
      example-b: # an installation name under gs.installations
        tokenUrl: https://dex.example-b.gigantic.io/token
        # Confidential client on that Dex (backend-only).
        clientId: ${CLUSTER_TOKEN_EXAMPLE_B_CLIENT_ID}
        clientSecret: ${CLUSTER_TOKEN_EXAMPLE_B_CLIENT_SECRET}
        # The Dex OIDC connector that trusts the portal's main Dex issuer.
        connectorId: portal
        # Scope of the issued id_token; the cross-client scope sets its
        # audience to the apiserver's client.
        scopes: openid email groups audience:server:client_id:dex-k8s-authenticator

An installation with a target is broker-covered without clusterTokenAudience. The browser learns only the installation names under targets; the endpoints and clients stay with the backend. Every field is required: a target without one fails the backend's startup.

Broker-covered installations

Each cluster is listed under gs.installations.<mc> and in kubernetes.clusterLocatorMethods. oidcTokenProvider: oidc-<mc> stays as the k8s-plugin routing key, but for a broker-covered cluster it needs no matching auth.providers entry. Setting clusterTokenAudience marks the installation as fully broker-covered, so its token is minted silently and it disappears from the provider settings page. An installation without it (and without a Dex target) is never exchanged at muster, which would only answer invalid_target; it keeps its own entry on the provider settings page.

gs:
  installations:
    example:
      authProvider: oidc
      oidcTokenProvider: oidc-example
      clusterTokenAudience: example # broker-covered
      pipeline: stable
      providers:
        - capa

Installations without Mimir (mimirEnabled)

Metrics-backed features (workload status, resource usage, HTTPRoute hostnames, node pool nodes) query the Giant Swarm observability stack at observability.<baseDomain>. Every Giant Swarm management cluster runs it, so the integration defaults to on. Standalone installations set

gs:
  installations:
    example:
      mimirEnabled: false

to opt out: the frontend then sends no Mimir queries for that installation and renders neutral "metrics unavailable" states instead of errors, and the backend Mimir proxy refuses queries for it. baseDomain cannot carry this meaning because other features (e.g. agent avatars) require it on every installation.

Cluster-access status element

A sidebar status element shows the per-installation access state -- connecting (the initial state while the first probe is in flight), healthy, degraded (with a reason such as "Token broker is unreachable" or "API unreachable (timeout)"), or session-expired (with a "Sign in again" action that triggers the single main login). It is fed by both the broker token flow and the clusters list, and requires no configuration.

Cluster access is established for every broker-covered installation on app load, regardless of which page you are on, so the status element is always visible and lists every covered installation from the start (instead of only appearing after you open the clusters page). The connections are re-checked on a slow interval to keep the status live and broker tokens warm. Only broker-covered installations (those with clusterTokenAudience set) are connected automatically, so this never triggers a per-cluster login popup.

When the main Dex session expires, the broker path triggers exactly one main login prompt and then resumes -- there are no per-cluster popups.

gs.kubernetes.proxyTimeoutMs

Bounds each Kubernetes proxy request (default 10000). An unreachable cluster becomes a fast, typed per-cluster error instead of freezing the whole clusters list; healthy clusters render immediately and the degraded one is marked in the status element and retried with capped backoff.

gs:
  kubernetes:
    proxyTimeoutMs: 10000

gs.kubernetes.proxyMaxConcurrency

Caps how many Kubernetes proxy requests run at once across the whole app, including broker token mints (default 6). Establishing access to every installation on startup would otherwise fan out into a storm of simultaneous connections that overwhelms the broker and the management-cluster apiservers, causing some requests to hit the proxy timeout before recovering on retry. Bounding concurrency smooths that storm; 6 matches the browser's own per-host connection limit, beyond which extra parallelism stops helping.

gs:
  kubernetes:
    proxyMaxConcurrency: 6

Login provider scopes

Every Giant Swarm OIDC provider requests openid profile email groups offline_access. That base set is fixed and no configuration removes a scope from it. Every other scope comes from gs.auth.extraScopes, which has no default, because a scope beyond the base set is a property of the issuer and of the clients that must accept the token:

gs:
  auth:
    # Applies to every login provider, the `mcp-*` ones included.
    extraScopes:
      - federated:id
      - audience:server:client_id:dex-k8s-authenticator

A Dex deployment needs both of the scopes above:

  • federated:id adds the federated_claims the main sign-in resolver reads to map a user onto a catalog entity. Without it the giantswarm-ad and giantswarm-github connector lookups never run, and every person signs in as user:oidc/<name> from the sub of the token instead of their catalog user.
  • audience:server:client_id:dex-k8s-authenticator is a cross-client scope that makes Dex name the dex-k8s-authenticator client in the audience of the token. Add one such scope per client whose audience a forwarded token must satisfy, and list the requesting client in the trustedPeers of that Dex client. A muster that trusts the dex-k8s-authenticator audience needs this scope, or it rejects the token the portal forwards to it.

Keycloak and Entra ID need no extra scope. They reject both scopes above with invalid_scope and show no login page, so leave gs.auth.extraScopes unset or set it to [].

A giantswarm-ad sign-in is looked up in the catalog by email, and a giantswarm-github sign-in by GitHub login, or by email when the token has no preferred_username. The giantswarm-simple-oidc and giantswarm-graveler-oidc connectors federate to the Dex of gazelle and graveler. Their sign-ins are looked up as if made through the connector the person used on that Dex, which the federated user_id names, so a person who signs in there with GitHub and a personal email still finds their catalog user. A sign-in through a Giant Swarm connector that matches no catalog user, or whose token carries neither a username nor a verified email to look up, is refused, and the sign-in dialog shows why.

A person who signs in through any other Dex connector, or through an issuer without federated_claims, resolves to user:oidc/<name>, where the name is a hash of the sub of the token. These users have no catalog entity, and a token without a sub is refused.

Changing the scopes on a running instance

Widening gs.auth.extraScopes (or gs.auth.scopes) signs everyone in again. An existing session was granted the previous scope set, and a token refresh cannot add a scope to that grant: the issuer re-issues the tokens with the scopes the sign-in consented to, whatever the refresh asks for. The login provider therefore refuses to refresh a session with fewer scopes than the configuration now requests, and the portal starts a fresh sign-in (a popup) on the person's next page load, asking for the new set. No manual sign-out is needed. Until that sign-in completes, the Agent Platform section explains any 401 an installation's API server answers with, and offers the sign-out.

Without the refusal an instance that gained the dex-k8s-authenticator audience scope kept refreshing the old grant: the backend answered the wider refresh request with a token that still lacked the audience, reported the requested scopes as granted, and every Kubernetes proxy read failed with 401 oidc: expected audience "dex-k8s-authenticator" until the person signed out by hand.

Keycloak needs one more step, on the IdP side: it has no built-in groups scope. Add a client scope named groups with a Group Membership mapper (claim groups, full path off) and attach it to the client as a default scope, or the login fails with invalid_scope on the base set as well.

Cluster details page resources

The cluster details page allows you to configure resource links that will be displayed in place of the default links.

The default links are the Grafana cluster overview dashboard and the Web UI. There is no default Alerts link: Grafana's alerting page is only enabled in the Giant Swarm organization (orgId=2), which customers can't access, so only a Giant Swarm staff instance configures one.

Configuration example

Below is the configuration of a Giant Swarm staff instance, which adds the Alerts link to the defaults. Leave the Alerts entry out on a customer instance:

gs:
  clusterDetails:
    resources:
      - label: 'Alerts'
        icon: 'NotificationsNone'
        url: 'https://grafana.${{BASE_DOMAIN}}/alerting?orgId=2'
      - label: 'Web UI'
        icon: 'Public'
        url: 'https://happa.${{BASE_DOMAIN}}/organizations/${{ORG_NAME}}/clusters/${{CLUSTER_NAME}}'
        clusterType: 'workload'
      - label: 'Web UI'
        icon: 'Public'
        url: 'https://happa.${{BASE_DOMAIN}}'
        clusterType: 'management'

Cluster type specific links

If the clusterType configuration option is not set, the link will be displayed for both management and workload clusters. If the clusterType is set to management, the link will be displayed only for management clusters. If the clusterType is set to workload, the link will be displayed only for workload clusters.

URL Templating

The url parameter supports templating with the following placeholders:

  • CLUSTER_NAME - name of the cluster.
  • CLUSTER_NAMESPACE - namespace of the cluster.
  • MC_NAME - name of the management cluster.
  • ORG_NAME - name of the organization.
  • BASE_DOMAIN - base domain of the management cluster.

For example, the URL https://happa.${{BASE_DOMAIN}}/organizations/${{ORG_NAME}}/clusters/${{CLUSTER_NAME}} will be dynamically populated with the appropriate values.

Icon Configuration

The icon parameter can be:

Deployment details page resources

The deployment details page allows you to configure resource links.

Configuration example

Below is an example configuration for the links on the deployment details page:

gs:
  deploymentDetails:
    resources:
      - label: 'Grafana \n dashboard'
        icon: 'GrafanaIcon'
        url: 'https://grafana.${{BASE_DOMAIN}}/d/abcdefghijklmn/web-application-vitals?var-cluster=${{APP_CLUSTER_NAME}}&var-namespace=${{APP_NAMESPACE}}&var-application=${{APP_NAME}}'

URL Templating

The url parameter supports templating with the following placeholders:

  • APP_CLUSTER_NAME - name of the deployment target cluster.
  • APP_NAME - name of the deployment.
  • APP_NAMESPACE - namespace of the deployment.
  • BASE_DOMAIN - base domain of the management cluster.
  • MC_NAME - name of the management cluster.

Icon Configuration

The icon parameter can be:

Home page resources

The home page allows you to configure resource links that will be displayed in place of the default links.

Configuration example

Below is an example configuration for overriding the default links on the home page:

gs:
  homepage:
    resources:
      - label: 'Giant Swarm \n GitHub'
        icon: 'GitHub'
        url: 'https://github.com/giantswarm'
      - label: 'Portal \n changelog'
        icon: 'BackstageIcon'
        url: 'https://github.com/giantswarm/backstage/releases'

Icon Configuration

The icon parameter can be:

Root redirect

By default / renders the home page. A deployment that ships one product instead of the whole portal can send / to that product's page:

app:
  rootRedirect: /catalog

The value is a path inside the app, not a URL. Unset keeps the home page. A value that does not start with /, or that is / itself, is ignored: the home page renders and the browser console logs a warning.

Two constraints:

  • Keep the home page extension enabled. It owns the / route, so page:home: false under app.extensions makes / a 404 page again and the redirect never runs.
  • The sidebar still shows the "Home" item, which now leads to the redirect target.

Optional features

Some optional features can be enabled and disabled via the app configuration. For example, the configuration snippet below will enable the installationsPage feature:

gs:
  features:
    installationsPage:
      enabled: true

The following optional features are available:

  • clustersPage: Enable the Clusters page, which lists all clusters -- both management and workload clusters -- in the installations the user has access to via the Backstage instance.
  • deploymentsPage: Enable the Deployments page, which lists all the deployments -- HelmRelease and App CR resources -- in the installations the user has access to via the Backstage instance.
  • installationsPage: Enable the Installations page, which lists all Resource entities of type instalation in the catalog.
  • scaffolder: Enables the scaffolder that lists available templates.

Egress through an HTTP proxy

Where the backend has to reach the internet through a forward proxy, set the GLOBAL_AGENT_* variables in the chart's backstage.extraEnvVars:

backstage:
  extraEnvVars:
    - name: GLOBAL_AGENT_HTTP_PROXY
      value: http://proxy.example.com:3128
    - name: GLOBAL_AGENT_NO_PROXY
      value: 127.0.0.1,localhost,.svc,.cluster.local,.example.com

They cover both HTTP stacks of the backend: global-agent routes the http/https modules, and the backend gives Node's native fetch (the catalog, the scaffolder and most plugins read URLs with it) a proxy dispatcher from the same values. GLOBAL_AGENT_HTTP_PROXY also serves HTTPS unless GLOBAL_AGENT_HTTPS_PROXY is set. Only GLOBAL_AGENT_NO_PROXY exempts hosts; a NO_PROXY injected into the pod does not apply. Write domain suffixes with a leading dot or *. (.example.com, *.example.com): both stacks read them as the domain's subdomains.

NODE_USE_ENV_PROXY=1 instead hands the proxy to Node itself (HTTP_PROXY, HTTPS_PROXY, NO_PROXY); the backend then leaves fetch to Node. Node's http module reads a bare example.com in NO_PROXY as that one host only, so domain suffixes need the leading dot there.

This mode needs a Node version within engines in the root package.json. On an older Node, native fetch would bypass the proxy: the backend loads undici 8, which replaces the proxy dispatcher Node installs with a direct one when Node's bundled undici is older than 7.27. The backend therefore refuses to start with NODE_USE_ENV_PROXY=1 on such a Node and names the version it needs. The GLOBAL_AGENT_* mode does not depend on the Node version.

Grafana dashboards card

Team and component pages carrying the grafana/dashboard-selector annotation get the dashboards card of @backstage-community/plugin-grafana only on a portal that enables it:

app:
  extensions:
    - entity-card:catalog/grafana-dashboards: true

The card is disabled by default because it works only where the plugin is wired: the grafana section names the host, and a proxy.endpoints entry at /grafana/api targets that host with a service-account token. The grafana section is required by the plugin's schema on every portal, so every deployment carries one, and the annotated entities reach every portal through the shared catalog. Without the switch an annotated entity shows neither the card nor the 404 it would report without the proxy entry.

proxy:
  endpoints:
    /grafana/api:
      target: https://grafana.example.com/
      headers:
        Authorization: Bearer ${GRAFANA_TOKEN}

grafana:
  hosts:
    - id: grafana
      domain: https://grafana.example.com

GitHub Actions and Pull Requests tabs

The GitHub Actions and Pull Requests tabs of a Component, and the GitHub Actions recent-runs card, read GitHub as the signed-in person. A portal offers them only where it has a GitHub login:

  • gs.github: the person's GitHub grant in muster, or
  • auth.providers.github: Backstage's own GitHub provider.

A portal with neither shows none of them, without any app.extensions entry: their login dialog could not succeed there, since the auth backend has no GitHub provider to start. app.extensions still turns them off where a GitHub login exists (entity-content:github-actions: false, entity-content:catalog/pull-requests: false).

Component dependency fetching

For the Giant Swarm devportal, we can enable asynchronous fetching of dependencies between components, based on the GitHub SBOM API. This will start updating dependency info once daily.

Only dependencies whose target component exists in the catalog are added to a component's dependsOn. Giant Swarm Go packages that have no catalog component (e.g. archived libraries) are skipped so they don't appear as dangling relations on the entity page.

catalog:
  processors:
    sbomDependencies:
      enabled: true
      schedule:
        frequency: { cron: '0 2 * * *' } # default: daily at 2 AM UTC
        timeout: { minutes: 60 }

Friendly labels and annotations

The friendlyLabels and friendlyAnnotations configuration allows you to control which Kubernetes resource labels/annotations are displayed and how they are formatted in the UI. Only items matching the configured patterns will be shown. If no configuration is provided, the following default is used for labels:

friendlyLabels:
  - label: 'giantswarm.io/service-priority'
    key: 'Service priority'
    valueMap:
      highest: 'Highest'
      medium: 'Medium'
      lowest: 'Lowest'

For annotations, there is no default configuration.

Configuration options

Each entry in friendlyLabels or friendlyAnnotations supports the following properties:

  • selector (string, required):
    • Used to match labels/annotations on resources. Can be:
      • An exact key (e.g. giantswarm.io/service-priority)
      • A key-value pair (e.g. giantswarm.io/service-priority:highest)
      • A pattern (e.g. giantswarm.io*) to match multiple labels/annotations by key prefix.
  • key (string, optional):
    • Overrides how the label/annotation key is displayed in the UI for matching items. If set, replaces the original key.
  • valueMap (object, optional):
    • Maps label/annotation values to display values. If an item's value matches a key in this map, it will be replaced with the mapped value. If not, the original value is shown.

Properties applicable only for friendlyLabels configuration:

  • variant (string, optional):
    • Applies additional visual styles to the label. Available variants are: gray, red, orange, yellow, green, teal, blue, purple, pink, brown.
    • See makeLabelVariants.ts for details.

Note:

  • The order of entries in friendlyLabels or friendlyAnnotations is important. Items are matched and displayed in the order they appear in the configuration.
  • Only labels/annotations matching a configured entry will be shown.

Example configuration

gs:
  friendlyAnnotations:
    - selector: 'cluster.giantswarm.io/description'
      key: 'Cluster description'
  friendlyLabels:
    - selector: 'giantswarm.io/service-priority'
      key: 'Service priority'
      valueMap:
        highest: 'Highest'
        medium: 'Medium'
        lowest: 'Lowest'
    - selector: 'environment'
      key: 'Environment'
      valueMap:
        prod: 'Production'
        dev: 'Development'
      variant: 'green'
    - selector: 'giantswarm.io*'
      variant: 'blue'

This configuration will:

  • Show only the specified labels and annotations.
  • Display custom keys and values where configured.
  • Apply visual variants for additional styling.