> ## Documentation Index
> Fetch the complete documentation index at: https://metoro.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Connect Monitored Clusters

> Install the Metoro exporter into monitored Kubernetes clusters using direct Helm values and an external token Secret

export const usePlannerSelections = () => {
  const [selections, setSelections] = useState(defaultSelections);
  useEffect(() => {
    setSelections(readSelections());
    const handleChange = event => {
      setSelections({
        ...defaultSelections,
        ...event.detail
      });
    };
    window.addEventListener(eventName, handleChange);
    return () => window.removeEventListener(eventName, handleChange);
  }, []);
  return [selections, setSelections];
};

export const storageKey = "metoro-onprem-installation-planner-v1";

export const selectedDecisionItems = selections => [...sectionOrder.map(section => ({
  title: sections[section].title,
  label: optionLabel(section, selections[section])
})), {
  title: "Deployment hostname",
  label: cleanHostname(selections.deploymentHostname) || "Not set"
}, ...selections.network === "splitHosts" ? [{
  title: "Ingester hostname",
  label: cleanHostname(selections.ingesterHostname) || "Not set"
}] : []];

export const sections = {
  topology: {
    title: "Topology",
    prompt: "Where will the hub run?",
    options: [{
      id: "dedicated",
      label: "Dedicated hub cluster",
      recommended: true,
      description: "Run the hub in its own cluster and connect monitored clusters outbound to it."
    }, {
      id: "shared",
      label: "Shared workload cluster",
      description: "Run the hub in the same cluster as monitored workloads for smaller deployments."
    }]
  },
  network: {
    title: "Network access",
    prompt: "How will ingress be exposed?",
    options: [{
      id: "sharedHost",
      label: "Shared host",
      recommended: true,
      description: "Use one deploymentUrl host with path routing for UI/API and ingestion."
    }, {
      id: "splitHosts",
      label: "Split hosts",
      description: "Use separate apiserver and ingester hostnames when routing or ownership differs."
    }]
  },
  urlScheme: {
    title: "URL scheme",
    prompt: "Will the ingress use TLS?",
    options: [{
      id: "https",
      label: "HTTPS",
      recommended: true,
      description: "Use https:// URLs and configure TLS for the browser-facing hub host."
    }, {
      id: "http",
      label: "HTTP",
      description: "Use http:// URLs when the browser-facing hub host is not served with TLS."
    }]
  },
  tls: {
    title: "TLS and trusted CAs",
    prompt: "Which certificate trust path will be used?",
    options: [{
      id: "publicCa",
      label: "Public CA",
      recommended: true,
      description: "Use certificates already trusted by browsers and container images."
    }, {
      id: "privateCa",
      label: "Private CA bundle required",
      description: "Make the CA bundle available before installing the hub and monitored-cluster exporters."
    }]
  },
  authentication: {
    title: "Authentication",
    prompt: "How will users authenticate?",
    options: [{
      id: "oidcGroups",
      label: "OIDC with group mappings",
      recommended: true,
      description: "Prepare issuer, client, callback URL, scopes, group claim, and role mappings."
    }, {
      id: "emailPassword",
      label: "Email/password local users",
      description: "Use the bootstrap admin and locally managed users without an external identity provider."
    }]
  },
  dependencies: {
    title: "Dependencies",
    prompt: "Who will run PostgreSQL and ClickHouse?",
    options: [{
      id: "bundled",
      label: "Bundled dependencies and operators",
      recommended: true,
      description: "Let the hub chart install the datastore operators and manage PostgreSQL, ClickHouse, Temporal, and datastore resources."
    }, {
      id: "preinstalledOperators",
      label: "Bundled dependencies, operators managed in house",
      description: "Use centrally managed CloudNativePG and Altinity operators while the hub chart manages its datastore resources."
    }, {
      id: "external",
      label: "Externally managed ClickHouse and PostgreSQL",
      description: "Use customer-managed datastores with existing operational ownership."
    }]
  },
  clickhouseStorage: {
    title: "ClickHouse storage",
    prompt: "Where will ClickHouse keep telemetry data?",
    options: [{
      id: "hotPvcObjectStorage",
      label: "PVCs + object storage",
      recommended: true,
      description: "Use retained PVCs for about one day of hot data and object storage for longer-term telemetry."
    }, {
      id: "persistentVolumesOnly",
      label: "PVCs only",
      description: "Keep all ClickHouse telemetry on retained persistent volumes."
    }]
  }
};

export const sectionOrder = ["topology", "network", "urlScheme", "tls", "authentication", "dependencies", "clickhouseStorage"];

export const readSelections = () => {
  if (typeof window === "undefined") {
    return defaultSelections;
  }
  try {
    const stored = window.localStorage.getItem(storageKey);
    if (!stored) {
      return defaultSelections;
    }
    return {
      ...defaultSelections,
      ...JSON.parse(stored)
    };
  } catch {
    return defaultSelections;
  }
};

export const optionLabel = (section, value) => {
  const config = sections[section];
  const option = config.options.find(item => item.id === value) || config.options.find(item => item.id === defaultSelections[section]);
  return option.label;
};

export const ingesterUrl = selections => {
  return `${deploymentScheme(selections)}://${ingesterHostname(selections)}`;
};

export const ingesterHostname = selections => {
  if (selections.network !== "splitHosts") {
    return deploymentHostname(selections);
  }
  return cleanHostname(selections.ingesterHostname) || "CHANGE_ME_INGESTER_HOSTNAME";
};

export const exporterOtlpUrl = selections => `${selections.network === "splitHosts" ? ingesterUrl(selections) : deploymentUrl(selections)}/ingest/api/v1/otel`;

export const exporterApiServerUrl = selections => `${deploymentUrl(selections)}/api/v1/exporter`;

export const eventName = "metoro-onprem-installation-planner-change";

export const deploymentUrl = selections => {
  return `${deploymentScheme(selections)}://${deploymentHostname(selections)}`;
};

export const deploymentScheme = selections => {
  return selections.urlScheme === "http" ? "http" : "https";
};

export const deploymentHostname = selections => {
  return cleanHostname(selections.deploymentHostname) || "CHANGE_ME_METORO_HOSTNAME";
};

export const defaultSelections = {
  topology: "dedicated",
  network: "sharedHost",
  urlScheme: "https",
  tls: "publicCa",
  authentication: "oidcGroups",
  dependencies: "bundled",
  clickhouseStorage: "hotPvcObjectStorage",
  deploymentHostname: "",
  ingesterHostname: ""
};

export const cleanHostname = value => {
  return String(value || "").trim().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
};

export const buildExporterValuesSkeleton = selections => {
  const privateCa = selections.tls === "privateCa";
  const lines = ["# metoro-exporter-values.yaml", "global:", "  # Optional: set this to a pull-through cache or mirror repository prefix.", "  # See the chart values.yaml for precedence details.", '  imageRepositoryOverride: ""', "", "exporter:", "  secret:", "    externalSecret:", "      enabled: true", "      name: metoro-exporter-token", "      secretKey: AUTH_TOKEN", "  envVars:", "    mandatory:", `      otlpUrl: "${exporterOtlpUrl(selections)}"`, `      apiServerUrl: "${exporterApiServerUrl(selections)}"`, '      shouldDropMetoroData: "false"'];
  if (privateCa) {
    lines.push("", "trustedCAs:", "  enabled: true", "  existingConfigMap: metoro-trusted-ca", "  key: ca.crt", "  mountPath: /etc/ssl/certs/metoro-onprem-ca-bundle.crt");
  }
  return lines.join("\n");
};

export const ConnectClustersValuesSkeleton = () => {
  const [selections] = usePlannerSelections();
  const values = buildExporterValuesSkeleton(selections);
  return <>
      <p>
        Save this content as <code>metoro-exporter-values.yaml</code>. The file
        contains endpoint and Secret references only; the bearer token stays in
        the Kubernetes Secret.
      </p>
      <pre>
        <code>{values}</code>
      </pre>
    </>;
};

export const ConnectClustersTrustedCA = () => {
  const [selections] = usePlannerSelections();
  if (selections.tls !== "privateCa") {
    return null;
  }
  return <section>
      <h3>Create the trusted CA ConfigMap</h3>
      <p>
        The exporter, collector, and node agent need the CA bundle before they
        connect to the hub URL. Create the ConfigMap in the exporter namespace
        before running Helm.
      </p>
      <pre>
        <code>{`kubectl -n metoro create configmap metoro-trusted-ca \\
  --from-file=ca.crt=/path/to/root-ca.pem`}</code>
      </pre>
    </section>;
};

export const ConnectClustersTokenSecretCommand = () => <pre>
    <code>{`kubectl -n metoro create secret generic metoro-exporter-token \\
  --from-literal=AUTH_TOKEN='CHANGE_ME_EXPORTER_BEARER_TOKEN'`}</code>
  </pre>;

export const ConnectClustersTokenSecret = () => {
  const [selections] = usePlannerSelections();
  const metoroUrl = deploymentUrl(selections);
  const ingestionSettingsUrl = `${metoroUrl}/settings?tab=ingestionSettings`;
  return <>
      <h3>Get the exporter bearer token from Metoro</h3>
      <ol>
        <li>
          Open the Metoro deployment URL and log in:
          <pre>
            <code>{metoroUrl}</code>
          </pre>
        </li>
        <li>
          Open Data Ingestion Settings from{" "}
          <strong>Settings -&gt; Data Ingestion</strong>, or use the direct URL:
          <pre>
            <code>{ingestionSettingsUrl}</code>
          </pre>
        </li>
        <li>
          In the Data Ingestion left menu, select{" "}
          <strong>Cluster Settings</strong>.
        </li>
        <li>
          Click <strong>Add Cluster</strong>.
        </li>
      </ol>

      <img src="/docs/docs/images/metoro-data-ingestion-cluster-settings.png" alt="Metoro Data Ingestion settings with Cluster Settings selected and Add Cluster highlighted" />

      <ol start="5">
        <li>
          On the <strong>Connect your Kubernetes cluster</strong> page, select{" "}
          <strong>Existing Cluster</strong>.
        </li>
        <li>
          Enter the cluster name, for example <code>cluster-1</code>.
        </li>
        <li>
          Copy the exporter bearer token.
        </li>
      </ol>

      <img src="/docs/docs/images/metoro-copy-exporter-bearer-token.png" alt="Metoro onboarding screen showing the copy exporter bearer token button" />

      <p>
        Use the copied exporter bearer token in the{" "}
        <code>metoro-exporter-token</code> Secret command below.
      </p>
    </>;
};

export const ConnectClustersSelectedDecisions = () => {
  const [selections] = usePlannerSelections();
  return <>
      <div>
        <p>
          These choices are saved in this browser. Change them on the{" "}
          <a href="/docs/on-premises/10.x/installation/planning">
            Installation Planning page
          </a>
          .
        </p>
      </div>

      <ul>
        {selectedDecisionItems(selections).map(item => <li key={item.title}>
            <strong>{item.title}:</strong> {item.label}
          </li>)}
      </ul>
    </>;
};

export const ConnectClustersPrerequisites = () => {
  const [selections] = usePlannerSelections();
  const items = ["Helm 3 installed on the machine running the install.", "kubectl access to the monitored cluster.", `Outbound access from the monitored cluster to ${exporterApiServerUrl(selections)} for exporter control-plane traffic.`, `Outbound access from the monitored cluster to ${exporterOtlpUrl(selections)} for telemetry ingestion.`];
  if (selections.tls === "privateCa") {
    items.push("The PEM CA bundle that signs the hub URL, ready to create as a ConfigMap in the exporter namespace.");
  }
  return <ul>
      {items.map(item => <li key={item}>{item}</li>)}
    </ul>;
};

export const ConnectClustersPostInstallChecks = () => {
  const commands = [{
    title: "Check Helm and pod readiness",
    command: "helm -n metoro status metoro-exporter\nkubectl -n metoro get pods"
  }, {
    title: "Check primary workloads",
    command: "kubectl -n metoro get deployment metoro-exporter\nkubectl -n metoro get daemonset metoro-node-agent\nkubectl -n metoro get statefulset\nkubectl -n metoro get hpa"
  }];
  return <>
      {commands.map(item => <section key={item.title}>
          <h3>{item.title}</h3>
          <pre>
            <code>{item.command}</code>
          </pre>
        </section>)}
    </>;
};

export const ConnectClustersNamespace = () => <>
    <p>
      Install monitored-cluster components into the <code>metoro</code>{" "}
      namespace. Use the same namespace for the exporter token Secret and any
      private CA ConfigMap.
    </p>
    <pre>
      <code>{`kubectl create namespace metoro`}</code>
    </pre>
  </>;

export const ConnectClustersHelmInstall = () => <>
    <p>
      If this machine is not already authenticated to the chart registry, log in
      with the credentials provided by Metoro.
    </p>
    <pre>
      <code>{`helm registry login quay.io`}</code>
    </pre>
    <p>
      Install the exporter chart with an explicit version and the generated
      values file.
    </p>
    <pre>
      <code>{`helm upgrade --install metoro-exporter \\
  oci://quay.io/metoro/charts/metoro-exporter-onprem \\
  --namespace metoro \\
  --version 10.4.0 \\
  --values metoro-exporter-values.yaml`}</code>
    </pre>
  </>;

export const ConnectClustersFinishOnboarding = () => {
  const [selections] = usePlannerSelections();
  const metoroUrl = deploymentUrl(selections);
  return <>
      <p>Go back to the Metoro UI:</p>
      <pre>
        <code>{metoroUrl}</code>
      </pre>
      <p>
        When the cluster is connected, the Metoro UI shows the connected cluster
        screen.
      </p>

      <img src="/docs/docs/images/metoro-connected-cluster-view.png" alt="Metoro onboarding screen showing a connected Kubernetes cluster" />

      <p>
        Continue the onboarding flow from the Metoro UI to set up Slack, GitHub,
        and AI features for your environment. If you do not want to add those
        integrations, click <strong>Return to Metoro</strong>.
      </p>
      <p>
        The new connected cluster becomes available in the Environment filter.
        In the top-right corner, select the connected environment as a global
        filter.
      </p>
      <img src="/docs/docs/images/metoro-environment-filter-connected-cluster.png" alt="Metoro global Environment filter showing the connected cluster" />
    </>;
};

export const ConnectClustersConnectionDetails = () => {
  const [selections] = usePlannerSelections();
  return <ul>
      <li>
        <strong>Exporter API URL:</strong>{" "}
        <code>{exporterApiServerUrl(selections)}</code>
      </li>
      <li>
        <strong>Telemetry ingest URL:</strong>{" "}
        <code>{exporterOtlpUrl(selections)}</code>
      </li>
    </ul>;
};

After the hub is running, install the exporter chart into each Kubernetes cluster you want to monitor. Each monitored cluster runs its own exporter, node agent, Redis, OpenTelemetry Collector, and target allocator. Those components collect local telemetry and send it outbound to the hub.

<Info>
  The install steps, endpoint values, and trust settings below use the decisions saved in Installation Planning.
</Info>

<Card title="Selected plan" icon="sliders">
  <ConnectClustersSelectedDecisions />
</Card>

<Steps>
  <Step title="Confirm prerequisites">
    Use a kube-context that points at the monitored cluster, not the hub cluster.

    <ConnectClustersPrerequisites />
  </Step>

  <Step title="Create the namespace">
    <ConnectClustersNamespace />
  </Step>

  <Step title="Prepare exporter inputs">
    The exporter needs an environment token and the hub endpoints it should call.

    <ConnectClustersConnectionDetails />

    <ConnectClustersTokenSecret />

    <Warning>
      The exporter bearer token is environment-name specific. If you change the
      cluster name, Metoro generates a different token. Use the token shown for
      the final cluster name.
    </Warning>

    <ConnectClustersTokenSecretCommand />

    <Warning>
      Treat the exporter bearer token like a secret. Do not commit it, put it in the values file, paste it into shared tickets, or store it in broadly accessible shell history.
    </Warning>

    <ConnectClustersTrustedCA />
  </Step>

  <Step title="Create the values file">
    <ConnectClustersValuesSkeleton />
  </Step>

  <Step title="Install with Helm">
    <ConnectClustersHelmInstall />
  </Step>

  <Step title="Run post-install checks">
    <ConnectClustersPostInstallChecks />
  </Step>

  <Step title="Go back to Metoro UI">
    <ConnectClustersFinishOnboarding />
  </Step>
</Steps>

## What Gets Installed

The exporter chart installs a cluster-local forwarding path. Metoro Exporter receives telemetry from the local collectors and node agents, watches Kubernetes resources, reads exporter configuration from the hub apiserver, and forwards telemetry to the hub ingester.

Node Agent runs as a privileged DaemonSet on monitored nodes. It needs host access, host filesystem mounts, and container runtime socket mounts so it can collect node and workload telemetry. It is not suitable for serverless node environments that do not expose the host to pods.

Redis is used for exporter coordination inside the monitored cluster. It does not store durable telemetry. If Redis state is lost, exporter-side maps are rebuilt as the exporter and node agents reconnect and observe the cluster again.

When ServiceMonitor scraping is enabled, the chart also installs an OpenTelemetry Collector and Target Allocator. The monitored cluster must already have the Prometheus Operator `ServiceMonitor` and `PodMonitor` CRDs installed for those resources to be discovered.

After the exporter connects, the monitored cluster should appear in the Metoro UI. It can take a few minutes for enough telemetry to arrive for services, workloads, and Kubernetes resources to populate.

## Troubleshooting

If exporter pods cannot reach the hub, check outbound network policy, DNS, and the exact hub URLs in `metoro-exporter-values.yaml`.

If pods fail with certificate errors and the hub uses private PKI, verify that the CA ConfigMap exists in the `metoro` namespace and that the values file references the same ConfigMap key.

If node-agent pods are missing from some nodes, check node selectors, tolerations, privileged workload policy, and whether those nodes expose the host paths and runtime sockets required by the DaemonSet.

If ServiceMonitor or PodMonitor targets are missing, confirm the Prometheus Operator CRDs exist in the monitored cluster and that the target allocator selectors match the resources you expect to scrape.
