forked from grafana/grafana
-
Notifications
You must be signed in to change notification settings - Fork 9
PMM-14993 One step install #894
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
Open
theTibi
wants to merge
24
commits into
main
Choose a base branch
from
one_step_install
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 12 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
dea75fe
PMM: Add Install PMM Client dropdown on Inventory Nodes page
theTibi 66449f6
PMM: Install Client menu creates node install token and copies command
theTibi e431e89
PMM: Label Nodes action as Add Node with MySQL/MongoDB/PostgreSQL menu
theTibi 4f2fc52
PMM: Add Valkey option to node installation menu and update command g…
theTibi 743dab9
Merge branch 'main' into one_step_install
theTibi 5ddc7d5
PMM: Update quick install command to use downloaded script and improv…
theTibi e71d808
Merge branch 'main' into one_step_install
theTibi 0d2a462
Merge branch 'main' into one_step_install
mattiasimonato 35a21a5
fix: apply Prettier to nodes tab utilities
mattiasimonato 6781820
fix: add unit tests for buildQuickInstallCommand
mattiasimonato 62a91be
PMM: Implement createNodeInstallToken function for Grafana service ac…
theTibi 064784e
PMM: Refactor API calls to use grafanaHttpApiUrl function
theTibi 6d52608
refactor: extract node install options constants and map icons by tech
Copilot 71f24b7
Merge branch 'main' into one_step_install
mattiasimonato b54b974
Merge branch 'main' into one_step_install
mattiasimonato 3acb5dc
fix(inventory): align quick install icons with Percona branding
mattiasimonato ccd755a
Merge branch 'main' into one_step_install
matejkubinec 05ed569
Merge branch 'main' into one_step_install
ademidoff ab40df6
Merge branch 'main' into one_step_install
mattiasimonato 8de4294
feat: unify design between services nodes, put more options into drop…
matejkubinec 04b613d
PMM-14441 PMM Dump encryption (#897)
matejkubinec bf6ddb4
Fix grafana stack height & back port fix from Grafana (#903)
fabio-silva a4fa43e
feat: provide data testid for add node button
matejkubinec cab7cd5
Merge branch 'main'
matejkubinec File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
public/app/percona/inventory/Tabs/NodesInstallCommand.utils.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { buildQuickInstallCommand, QuickInstallTech } from './NodesInstallCommand.utils'; | ||
|
|
||
| describe('buildQuickInstallCommand', () => { | ||
| it('wraps the downloaded script path in single quotes', () => { | ||
| const cmd = buildQuickInstallCommand('mysql'); | ||
| expect(cmd).toContain("'/tmp/install-pmm-client.sh'"); | ||
| }); | ||
|
|
||
| it('URL-encodes the token before embedding it in --pmm-server-url', () => { | ||
| const cmd = buildQuickInstallCommand('mysql', 'abc:def@ghi/'); | ||
| expect(cmd).toContain('service_token:abc%3Adef%40ghi%2F@'); | ||
| }); | ||
|
|
||
| it('emits <TOKEN> placeholder when no token is provided', () => { | ||
| const cmd = buildQuickInstallCommand('mysql'); | ||
| expect(cmd).toContain('service_token:<TOKEN>@'); | ||
| }); | ||
|
|
||
| it.each<QuickInstallTech>(['mysql', 'postgresql', 'mongodb', 'valkey'])( | ||
| 'includes the selected tech in the --tech flag (%s)', | ||
| (tech) => { | ||
| const cmd = buildQuickInstallCommand(tech); | ||
| expect(cmd).toContain(`--tech '${tech}'`); | ||
| } | ||
| ); | ||
| }); |
37 changes: 37 additions & 0 deletions
37
public/app/percona/inventory/Tabs/NodesInstallCommand.utils.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| export type QuickInstallTech = 'mysql' | 'postgresql' | 'mongodb' | 'valkey'; | ||
|
|
||
| const shellEscape = (value: string): string => `'${value.replace(/'/g, `'\\''`)}'`; | ||
|
|
||
| /** Same path as PMM UI "Prompt on node" / install-pmm-client.sh docs. */ | ||
| const DOWNLOADED_SCRIPT_PATH = '/tmp/install-pmm-client.sh'; | ||
|
|
||
| /** | ||
| * Minimal install command: install pmm-client, configure pmm-agent, add selected DB with defaults. | ||
| * Uses download-then-run (not curl|bash) so install-pmm-client.sh can prompt for DB credentials on a TTY. | ||
| * If DB_USER / DB_PASSWORD (or per-tech MYSQL_* / …) are already exported, `sudo -E` preserves them and prompts are skipped. | ||
| * | ||
| * Aligns with PMM Install PMM Client "Prompt on node" command shape. | ||
| */ | ||
| export function buildQuickInstallCommand(tech: QuickInstallTech, token?: string): string { | ||
| if (typeof window === 'undefined') { | ||
|
mattiasimonato marked this conversation as resolved.
|
||
| return ''; | ||
| } | ||
|
|
||
| const protocol = window.location.protocol; | ||
| const host = window.location.hostname; | ||
| const port = window.location.port || (protocol === 'https:' ? '443' : '80'); | ||
| const origin = `${protocol}//${window.location.host}`; | ||
| const scriptUrl = `${origin}/pmm-static/install-pmm-client.sh`; | ||
| const tokenForUrl = token ? encodeURIComponent(token) : '<TOKEN>'; | ||
| const serverUrl = `https://service_token:${tokenForUrl}@${host}:${port}`; | ||
|
mattiasimonato marked this conversation as resolved.
|
||
|
|
||
| const curl = `curl -fsSLk -o ${shellEscape(DOWNLOADED_SCRIPT_PATH)} ${shellEscape(scriptUrl)}`; | ||
|
|
||
| const flags = [ | ||
| `--pmm-server-url ${shellEscape(serverUrl)}`, | ||
| `--tech ${shellEscape(tech)}`, | ||
| '--pmm-server-insecure-tls', | ||
| ]; | ||
|
|
||
| return [curl, `sudo -E bash ${shellEscape(DOWNLOADED_SCRIPT_PATH)} \\`, ` ${flags.join(' \\\n ')}`].join('\n'); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| import { config } from '@grafana/runtime'; | ||
|
|
||
| import { api } from 'app/percona/shared/helpers/api'; | ||
|
|
||
| /** | ||
| * Grafana HTTP API path when `serve_from_sub_path` is true (PMM sets | ||
| * `root_url = https://…/graph`). Browser requests must use `/graph/api/…`, | ||
| * not `/api/…` — the latter hit the host root where nginx does not proxy | ||
| * to Grafana, so service-account calls would fail with 404. | ||
| * | ||
| * @param restPath Path after `/api`, e.g. `/serviceaccounts/search`. | ||
| */ | ||
| function grafanaHttpApiUrl(restPath: string): string { | ||
| const p = restPath.startsWith('/') ? restPath : `/${restPath}`; | ||
| return `${config.appSubUrl ?? ''}/api${p}`; | ||
| } | ||
|
|
||
| /** | ||
| * Mints a short-lived Grafana service-account token used by the "Add Node" | ||
| * quick-install command (one-step `install-pmm-client.sh` flow). | ||
| * | ||
| * Implementation note: this used to POST to a PMM backend endpoint | ||
| * (/v1/management/nodes:installToken). That endpoint was removed because | ||
| * the same job can be done by calling Grafana's serviceaccounts API | ||
| * directly — we're already running inside Grafana with the user's session | ||
| * cookie, so there is no extra trust boundary to add. Grafana enforces | ||
| * Admin-only access to `{appSubUrl}/api/serviceaccounts/*`, which is the | ||
| * same gate the removed backend had. | ||
| * | ||
| * Lifecycle mirrors what the old backend did: | ||
| * - One shared service account per technology ("pmm-install-sa-<tech>"), | ||
| * created lazily on first use; we don't want one SA per token because | ||
| * they would accumulate in Grafana forever. | ||
| * - Each call mints a brand-new token on that SA with a UUID-suffixed | ||
| * name so concurrent calls don't collide on Grafana's unique-name | ||
| * constraint per SA. | ||
| * - Tokens are short-lived (15 min hard cap, matching the previous | ||
| * server-side cap). Re-run the action to get a fresh one. | ||
| */ | ||
| export interface CreateNodeInstallTokenResponse { | ||
| token: string; | ||
| expiresAt: string; | ||
| } | ||
|
|
||
| const MAX_TTL_SECONDS = 15 * 60; | ||
| const DEFAULT_TTL_SECONDS = 15 * 60; | ||
|
|
||
| const SUPPORTED_TECHNOLOGIES = new Set(['mysql', 'postgresql', 'mongodb', 'valkey']); | ||
|
|
||
| const SA_NAME_PREFIX = 'pmm-install-sa'; | ||
| const TOKEN_NAME_PREFIX = 'pmm-install-st'; | ||
|
|
||
| interface GrafanaServiceAccount { | ||
| id: number; | ||
| name: string; | ||
| } | ||
|
|
||
| interface GrafanaServiceAccountSearch { | ||
| totalCount: number; | ||
| serviceAccounts: GrafanaServiceAccount[]; | ||
| } | ||
|
|
||
| interface GrafanaTokenResponse { | ||
| id: number; | ||
| name: string; | ||
| key: string; | ||
| } | ||
|
|
||
| export async function createNodeInstallToken( | ||
| technology: string, | ||
| ttlSeconds = 0 | ||
| ): Promise<CreateNodeInstallTokenResponse> { | ||
| if (!SUPPORTED_TECHNOLOGIES.has(technology)) { | ||
| throw new Error(`unsupported technology "${technology}"`); | ||
| } | ||
|
|
||
| let ttl = ttlSeconds > 0 ? ttlSeconds : DEFAULT_TTL_SECONDS; | ||
| if (ttl > MAX_TTL_SECONDS) { | ||
| ttl = MAX_TTL_SECONDS; | ||
| } | ||
|
|
||
| const saName = `${SA_NAME_PREFIX}-${technology}`; | ||
|
|
||
| let saId = await findServiceAccountIdByName(saName); | ||
| if (saId === null) { | ||
| saId = await createServiceAccount(saName); | ||
| } | ||
|
|
||
| const tokenName = `${TOKEN_NAME_PREFIX}-${technology}-${crypto.randomUUID()}`; | ||
| const key = await mintToken(saId, tokenName, ttl); | ||
|
|
||
| return { | ||
| token: key, | ||
| expiresAt: new Date(Date.now() + ttl * 1000).toISOString(), | ||
| }; | ||
| } | ||
|
|
||
| async function findServiceAccountIdByName(name: string): Promise<number | null> { | ||
| // Pass disableNotifications=true on all three calls so the single friendly | ||
| // "Could not create install token" toast in the caller is the only one the | ||
| // user sees on failure (api.post would otherwise emit its own toast). | ||
| const res = await api.get<GrafanaServiceAccountSearch, { query: string }>( | ||
| grafanaHttpApiUrl('/serviceaccounts/search'), | ||
| true, | ||
| { params: { query: name } } | ||
| ); | ||
| const match = res.serviceAccounts?.find((sa) => sa.name === name) ?? null; | ||
| return match ? match.id : null; | ||
| } | ||
|
|
||
| async function createServiceAccount(name: string): Promise<number> { | ||
| // Admin role required — `pmm-admin config` / inventory writes need it in | ||
| // real PMM setups, and Grafana itself only lets Admins POST to this route. | ||
| const res = await api.post<GrafanaServiceAccount, { name: string; role: string; isDisabled: boolean }>( | ||
| grafanaHttpApiUrl('/serviceaccounts'), | ||
| { name, role: 'Admin', isDisabled: false }, | ||
| true | ||
| ); | ||
| return res.id; | ||
| } | ||
|
|
||
| async function mintToken(serviceAccountId: number, tokenName: string, ttlSeconds: number): Promise<string> { | ||
| // Only name + secondsToLive — passing extra fields (e.g. role) has been | ||
| // observed to make some Grafana versions ignore secondsToLive and fall | ||
| // back to a long default expiry. | ||
| const res = await api.post<GrafanaTokenResponse, { name: string; secondsToLive: number }>( | ||
| grafanaHttpApiUrl(`/serviceaccounts/${serviceAccountId}/tokens`), | ||
| { name: tokenName, secondsToLive: ttlSeconds }, | ||
| true | ||
| ); | ||
| return res.key; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.