Skip to content

Commit

Permalink
feat: enable Wrangler to target the staging API by setting WRANGLER_A…
Browse files Browse the repository at this point in the history
…PI_ENVIRONMENT=staging

If you are developing Wrangler, or an internal Cloudflare feature, and during testing,
need Wrangler to target the staging API rather than production, it is now possible by
setting the `WRANGLER_API_ENVIRONMENT` environment variable to `staging`.

This will update all the necessary OAuth and API URLs, update the OAuth client ID, and
also (if necessary) acquire an Access token for to get through the firewall to the
staging URLs.
  • Loading branch information
petebacondarwin committed Dec 12, 2022
1 parent 6f2ae24 commit cd760bd
Show file tree
Hide file tree
Showing 15 changed files with 280 additions and 104 deletions.
13 changes: 13 additions & 0 deletions .changeset/brave-cycles-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"wrangler": patch
---

feat: enable Wrangler to target the staging API by setting WRANGLER_API_ENVIRONMENT=staging

If you are developing Wrangler, or an internal Cloudflare feature, and during testing,
need Wrangler to target the staging API rather than production, it is now possible by
setting the `WRANGLER_API_ENVIRONMENT` environment variable to `staging`.

This will update all the necessary OAuth and API URLs, update the OAuth client ID, and
also (if necessary) acquire an Access token for to get through the firewall to the
staging URLs.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"TESTTEXTBLOBNAME",
"TESTWASMNAME",
"extensionless",
"webcontainer",
"yxxx"
],
"eslint.runtime": "node",
Expand Down
2 changes: 1 addition & 1 deletion packages/wrangler/src/__tests__/helpers/mock-cfetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Readable } from "node:stream";
import { URL, URLSearchParams } from "node:url";
import { pathToRegexp } from "path-to-regexp";
import { Response } from "undici";
import { getCloudflareApiBaseUrl } from "../../cfetch";
import { getCloudflareApiBaseUrl } from "../../environment-variables/misc-variables";
import type { FetchResult, FetchError } from "../../cfetch";
import type {
fetchInternal,
Expand Down
4 changes: 0 additions & 4 deletions packages/wrangler/src/__tests__/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
fetchInternal,
fetchKVGetValue,
fetchR2Objects,
getCloudflareAPIBaseURL,
performApiFetch,
} from "../cfetch/internal";
import { confirm, prompt } from "../dialogs";
Expand Down Expand Up @@ -93,9 +92,6 @@ afterAll(() => msw.close());
jest.mock("../cfetch/internal");
(fetchInternal as jest.Mock).mockImplementation(mockFetchInternal);
(fetchKVGetValue as jest.Mock).mockImplementation(mockFetchKVGetValue);
(getCloudflareAPIBaseURL as jest.Mock).mockReturnValue(
"https://api.cloudflare.com/client/v4"
);
(fetchR2Objects as jest.Mock).mockImplementation(mockFetchR2Objects);
(fetchDashboardScript as jest.Mock).mockImplementation(mockFetchDashScript);
(performApiFetch as jest.Mock).mockImplementation(mockPerformApiFetch);
Expand Down
2 changes: 0 additions & 2 deletions packages/wrangler/src/cfetch/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import type { RequestInit } from "undici";

// Check out https://api.cloudflare.com/ for API docs.

export { getCloudflareAPIBaseURL as getCloudflareApiBaseUrl } from "./internal";

export interface FetchError {
code: number;
message: string;
Expand Down
21 changes: 6 additions & 15 deletions packages/wrangler/src/cfetch/internal.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,14 @@
import assert from "node:assert";
import { fetch, File, Headers } from "undici";
import { version as wranglerVersion } from "../../package.json";
import { getEnvironmentVariableFactory } from "../environment-variables";
import { getCloudflareApiBaseUrl } from "../environment-variables/misc-variables";
import { logger } from "../logger";
import { ParseError, parseJSON } from "../parse";
import { loginOrRefreshIfRequired, requireApiToken } from "../user";
import type { ApiCredentials } from "../user";
import type { URLSearchParams } from "node:url";
import type { RequestInit, HeadersInit, Response } from "undici";

/**
* Get the URL to use to access the Cloudflare API.
*/
export const getCloudflareAPIBaseURL = getEnvironmentVariableFactory({
variableName: "CLOUDFLARE_API_BASE_URL",
deprecatedName: "CF_API_BASE_URL",
defaultValue: "https://api.cloudflare.com/client/v4",
});

/*
* performApiFetch does everything required to make a CF API request,
* but doesn't parse the response as JSON. For normal V4 API responses,
Expand All @@ -42,7 +33,7 @@ export async function performApiFetch(

const queryString = queryParams ? `?${queryParams.toString()}` : "";
logger.debug(
`-- START CF API REQUEST: ${method} ${getCloudflareAPIBaseURL()}${resource}${queryString}`
`-- START CF API REQUEST: ${method} ${getCloudflareApiBaseUrl()}${resource}${queryString}`
);
const logHeaders = cloneHeaders(headers);
delete logHeaders["Authorization"];
Expand All @@ -52,7 +43,7 @@ export async function performApiFetch(
JSON.stringify({ ...init, headers: logHeaders }, null, 2)
);
logger.debug("-- END CF API REQUEST");
return await fetch(`${getCloudflareAPIBaseURL()}${resource}${queryString}`, {
return await fetch(`${getCloudflareApiBaseUrl()}${resource}${queryString}`, {
method,
...init,
headers,
Expand Down Expand Up @@ -173,7 +164,7 @@ export async function fetchKVGetValue(
const auth = requireApiToken();
const headers: Record<string, string> = {};
addAuthorizationHeaderIfUnspecified(headers, auth);
const resource = `${getCloudflareAPIBaseURL()}/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${key}`;
const resource = `${getCloudflareApiBaseUrl()}/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${key}`;
const response = await fetch(resource, {
method: "GET",
headers,
Expand Down Expand Up @@ -206,7 +197,7 @@ export async function fetchR2Objects(
addAuthorizationHeaderIfUnspecified(headers, auth);
addUserAgent(headers);

const response = await fetch(`${getCloudflareAPIBaseURL()}${resource}`, {
const response = await fetch(`${getCloudflareApiBaseUrl()}${resource}`, {
...bodyInit,
headers,
});
Expand All @@ -233,7 +224,7 @@ export async function fetchDashboardScript(
addAuthorizationHeaderIfUnspecified(headers, auth);
addUserAgent(headers);

const response = await fetch(`${getCloudflareAPIBaseURL()}${resource}`, {
const response = await fetch(`${getCloudflareApiBaseUrl()}${resource}`, {
...bodyInit,
headers,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { logger } from "./logger";
import { logger } from "../logger";

type VariableNames =
| "CLOUDFLARE_ACCOUNT_ID"
Expand All @@ -7,7 +7,13 @@ type VariableNames =
| "CLOUDFLARE_EMAIL"
| "WRANGLER_SEND_METRICS"
| "CLOUDFLARE_API_BASE_URL"
| "WRANGLER_LOG";
| "WRANGLER_LOG"
| "WRANGLER_API_ENVIRONMENT"
| "WRANGLER_CLIENT_ID"
| "WRANGLER_AUTH_URL"
| "WRANGLER_TOKEN_URL"
| "WRANGLER_REVOKE_URL"
| "WRANGLER_CF_AUTHORIZATION_TOKEN";

type DeprecatedNames =
| "CF_ACCOUNT_ID"
Expand All @@ -29,6 +35,7 @@ export function getEnvironmentVariableFactory({
variableName: VariableNames;
deprecatedName?: DeprecatedNames;
}): () => string | undefined;

/**
* Create a function used to access an environment variable, with a default value.
*
Expand All @@ -44,6 +51,7 @@ export function getEnvironmentVariableFactory({
deprecatedName?: DeprecatedNames;
defaultValue: string;
}): () => string;

/**
* Create a function used to access an environment variable.
*
Expand Down
31 changes: 31 additions & 0 deletions packages/wrangler/src/environment-variables/misc-variables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { getEnvironmentVariableFactory } from "./factory";

/**
* `WRANGLER_SEND_METRICS` can override whether we attempt to send metrics information to Sparrow.
*/
export const getWranglerSendMetricsFromEnv = getEnvironmentVariableFactory({
variableName: "WRANGLER_SEND_METRICS",
});

/**
* Set `WRANGLER_API_ENVIRONMENT` environment variable to "staging" to tell Wrangler to hit the staging APIs rather than production.
*/
export const getCloudflareApiEnvironmentFromEnv = () => {
const env = getEnvironmentVariableFactory({
variableName: "WRANGLER_API_ENVIRONMENT",
defaultValue: "production",
})();
return env;
};

/**
* `CLOUDFLARE_API_BASE_URL` specifies the URL to the Cloudflare API.
*/
export const getCloudflareApiBaseUrl = getEnvironmentVariableFactory({
variableName: "CLOUDFLARE_API_BASE_URL",
deprecatedName: "CF_API_BASE_URL",
defaultValue:
getCloudflareApiEnvironmentFromEnv() === "staging"
? "https://api.staging.cloudflare.com/client/v4"
: "https://api.cloudflare.com/client/v4",
});
2 changes: 1 addition & 1 deletion packages/wrangler/src/logger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { format } from "node:util";
import { formatMessagesSync } from "esbuild";
import { getEnvironmentVariableFactory } from "./environment-variables";
import { getEnvironmentVariableFactory } from "./environment-variables/factory";
import type { BuildFailure } from "esbuild";

export const LOGGER_LEVELS = {
Expand Down
2 changes: 1 addition & 1 deletion packages/wrangler/src/metrics/metrics-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import path from "node:path";
import { fetchResult } from "../cfetch";
import { getConfigCache, saveToConfigCache } from "../config-cache";
import { confirm } from "../dialogs";
import { getEnvironmentVariableFactory } from "../environment-variables";
import { getEnvironmentVariableFactory } from "../environment-variables/factory";
import { getGlobalWranglerConfigPath } from "../global-wrangler-config-path";
import { CI } from "../is-ci";
import isInteractive from "../is-interactive";
Expand Down
151 changes: 151 additions & 0 deletions packages/wrangler/src/user/auth-variables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { execaSync } from "execa";
import { getEnvironmentVariableFactory } from "../environment-variables/factory";
import { getCloudflareApiEnvironmentFromEnv } from "../environment-variables/misc-variables";
import { logger } from "../logger";

/**
* `CLOUDFLARE_ACCOUNT_ID` overrides the account inferred from the current user.
*/
export const getCloudflareAccountIdFromEnv = getEnvironmentVariableFactory({
variableName: "CLOUDFLARE_ACCOUNT_ID",
deprecatedName: "CF_ACCOUNT_ID",
});

export const getCloudflareAPITokenFromEnv = getEnvironmentVariableFactory({
variableName: "CLOUDFLARE_API_TOKEN",
deprecatedName: "CF_API_TOKEN",
});
export const getCloudflareGlobalAuthKeyFromEnv = getEnvironmentVariableFactory({
variableName: "CLOUDFLARE_API_KEY",
deprecatedName: "CF_API_KEY",
});
export const getCloudflareGlobalAuthEmailFromEnv =
getEnvironmentVariableFactory({
variableName: "CLOUDFLARE_EMAIL",
deprecatedName: "CF_EMAIL",
});

/**
* `WRANGLER_CLIENT_ID` is a UUID that is used to identify Wrangler
* to the Cloudflare APIs.
*
* Normally you should not need to set this explicitly.
* If you want to switch to the staging environment set the
* `WRANGLER_USE_STAGING` environment variable instead.
*/
export const getClientIdFromEnv = getEnvironmentVariableFactory({
variableName: "WRANGLER_CLIENT_ID",
defaultValue:
getCloudflareApiEnvironmentFromEnv() === "staging"
? "4b2ea6cc-9421-4761-874b-ce550e0e3def"
: "54d11594-84e4-41aa-b438-e81b8fa78ee7",
});

/**
* `WRANGLER_AUTH_URL` is the path that is used to access OAuth
* for the Cloudflare APIs.
*
* Normally you should not need to set this explicitly.
* If you want to switch to the staging environment set the
* `WRANGLER_USE_STAGING` environment variable instead.
*/
export const getAuthUrlFromEnv = getEnvironmentVariableFactory({
variableName: "WRANGLER_AUTH_URL",
defaultValue:
getCloudflareApiEnvironmentFromEnv() === "staging"
? "https://dash.staging.cloudflare.com/oauth2/auth"
: "https://dash.cloudflare.com/oauth2/auth",
});

/**
* `WRANGLER_TOKEN_URL` is the path that is used to exchange an OAuth
* token for an API token.
*
* Normally you should not need to set this explicitly.
* If you want to switch to the staging environment set the
* `WRANGLER_USE_STAGING` environment variable instead.
*/
export const getTokenUrlFromEnv = getEnvironmentVariableFactory({
variableName: "WRANGLER_TOKEN_URL",
defaultValue:
getCloudflareApiEnvironmentFromEnv() === "staging"
? "https://dash.staging.cloudflare.com/oauth2/token"
: "https://dash.cloudflare.com/oauth2/token",
});

/**
* `WRANGLER_REVOKE_URL` is the path that is used to exchange an OAuth
* refresh token for a new OAuth token.
*
* Normally you should not need to set this explicitly.
* If you want to switch to the staging environment set the
* `WRANGLER_USE_STAGING` environment variable instead.
*/
export const getRevokeUrlFromEnv = getEnvironmentVariableFactory({
variableName: "WRANGLER_REVOKE_URL",
defaultValue:
getCloudflareApiEnvironmentFromEnv() === "staging"
? "https://dash.staging.cloudflare.com/oauth2/revoke"
: "https://dash.cloudflare.com/oauth2/revoke",
});

/**
* Set the `WRANGLER_CF_AUTHORIZATION_TOKEN` to the CF_Authorization token found at https://dash.staging.cloudflare.com/bypass-limits
* if you want to access the staging environment, triggered by `WRANGLER_API_ENVIRONMENT=staging`.
*/
export const getCloudflareAccessToken = () => {
const env = getEnvironmentVariableFactory({
variableName: "WRANGLER_CF_AUTHORIZATION_TOKEN",
})();

// If the environment variable is defined, go ahead and use it.
if (env !== undefined) {
return env;
}

const cloudflareAuthHost = new URL(getTokenUrlFromEnv()).host;

// Try to get the access token via cloudflared
try {
const { stdout: token } = execaSync("cloudflared", [
`access`,
`token`,
`--app`,
cloudflareAuthHost,
]);
if (
!token.includes(
"Unable to find token for provided application. Please run login command to generate token."
)
) {
return token;
}
} catch (e) {
// OK that didn't work... move on.
logger.debug(e);
}

// No luck. Let's try to get it by logging in via cloudflared.
try {
const { stdout: login } = execaSync("cloudflared", [
`access`,
`login`,
cloudflareAuthHost,
]);
const match = /Successfully fetched your token:\s+(.+)/.exec(login);
if (match) {
console.log(match);
return match[1];
}
} catch (e) {
// Didn't work either... moving along.
logger.debug(e);
}
// Still no luck give up and give the user some ideas of next steps.
throw Error(
"When trying to access staging environment we need an 'access token'.\n" +
"We were unable to get one automatically using cloudflared. Run with debug logging to see more detailed errors.\n" +
"Alternatively, you could provide your own access token by setting the WRANGLER_CF_AUTHORIZATION_TOKEN environment variable\n" +
"to a token that is generated at https://dash.staging.cloudflare.com/bypass-limits."
);
};
2 changes: 1 addition & 1 deletion packages/wrangler/src/user/choose-account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SelectInput from "ink-select-input";
import React from "react";
import { fetchListResult } from "../cfetch";
import { logger } from "../logger";
import { getCloudflareAccountIdFromEnv } from "./env-vars";
import { getCloudflareAccountIdFromEnv } from "./auth-variables";

export type ChooseAccountItem = {
id: string;
Expand Down
Loading

0 comments on commit cd760bd

Please sign in to comment.