Add a feature flag to override Juno endpoint (#700)

* Add a feature flag to override Juno endpoint

* Fix build
This commit is contained in:
Tanuj Mittal 2021-04-19 17:38:53 -07:00 committed by GitHub
parent f2585bba14
commit 914e969083
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 60 additions and 20 deletions

View File

@ -26,6 +26,7 @@ export interface ConfigContext {
GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it. GITHUB_CLIENT_SECRET?: string; // No need to inject secret for prod. Juno already knows it.
hostedExplorerURL: string; hostedExplorerURL: string;
armAPIVersion?: string; armAPIVersion?: string;
allowedJunoOrigins: string[];
} }
// Default configuration // Default configuration
@ -53,6 +54,13 @@ let configContext: Readonly<ConfigContext> = {
GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306 GITHUB_CLIENT_ID: "6cb2f63cf6f7b5cbdeca", // Registered OAuth app: https://github.com/settings/applications/1189306
JUNO_ENDPOINT: "https://tools.cosmos.azure.com", JUNO_ENDPOINT: "https://tools.cosmos.azure.com",
BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com", BACKEND_ENDPOINT: "https://main.documentdb.ext.azure.com",
allowedJunoOrigins: [
"https://juno-test.documents-dev.windows-int.net",
"https://juno-test2.documents-dev.windows-int.net",
"https://tools.cosmos.azure.com",
"https://tools-staging.cosmos.azure.com",
"https://localhost",
],
}; };
export function resetConfigContext(): void { export function resetConfigContext(): void {
@ -86,13 +94,18 @@ export async function initializeConfiguration(): Promise<ConfigContext> {
}); });
if (response.status === 200) { if (response.status === 200) {
try { try {
const { allowedParentFrameOrigins, ...externalConfig } = await response.json(); const { allowedParentFrameOrigins, allowedJunoOrigins, ...externalConfig } = await response.json();
Object.assign(configContext, externalConfig); Object.assign(configContext, externalConfig);
if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) { if (allowedParentFrameOrigins && allowedParentFrameOrigins.length > 0) {
updateConfigContext({ updateConfigContext({
allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins], allowedParentFrameOrigins: [...configContext.allowedParentFrameOrigins, ...allowedParentFrameOrigins],
}); });
} }
if (allowedJunoOrigins && allowedJunoOrigins.length > 0) {
updateConfigContext({
allowedJunoOrigins: [...configContext.allowedJunoOrigins, ...allowedJunoOrigins],
});
}
} catch (error) { } catch (error) {
console.error("Unable to parse json in config file"); console.error("Unable to parse json in config file");
console.error(error); console.error(error);

View File

@ -1,10 +1,9 @@
import ko from "knockout"; import ko from "knockout";
import { HttpHeaders, HttpStatusCodes } from "../Common/Constants"; import { HttpHeaders, HttpStatusCodes } from "../Common/Constants";
import { IPinnedRepo, JunoClient, IPublishNotebookRequest } from "./JunoClient";
import { configContext } from "../ConfigContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { DatabaseAccount } from "../Contracts/DataModels"; import { DatabaseAccount } from "../Contracts/DataModels";
import { updateUserContext, userContext } from "../UserContext"; import { updateUserContext, userContext } from "../UserContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { IPinnedRepo, IPublishNotebookRequest, JunoClient } from "./JunoClient";
const sampleSubscriptionId = "subscriptionId"; const sampleSubscriptionId = "subscriptionId";
@ -157,7 +156,7 @@ describe("Gallery", () => {
const response = await junoClient.getSampleNotebooks(); const response = await junoClient.getSampleNotebooks();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/samples`, undefined); expect(window.fetch).toBeCalledWith(`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/samples`, undefined);
}); });
it("getPublicNotebooks", async () => { it("getPublicNotebooks", async () => {
@ -169,7 +168,7 @@ describe("Gallery", () => {
const response = await junoClient.getPublicNotebooks(); const response = await junoClient.getPublicNotebooks();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/public`, undefined); expect(window.fetch).toBeCalledWith(`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/public`, undefined);
}); });
it("getNotebook", async () => { it("getNotebook", async () => {
@ -182,7 +181,7 @@ describe("Gallery", () => {
const response = await junoClient.getNotebookInfo(id); const response = await junoClient.getNotebookInfo(id);
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}`); expect(window.fetch).toBeCalledWith(`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/${id}`);
}); });
it("getNotebookContent", async () => { it("getNotebookContent", async () => {
@ -195,7 +194,7 @@ describe("Gallery", () => {
const response = await junoClient.getNotebookContent(id); const response = await junoClient.getNotebookContent(id);
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/content`); expect(window.fetch).toBeCalledWith(`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/${id}/content`);
}); });
it("increaseNotebookViews", async () => { it("increaseNotebookViews", async () => {
@ -208,7 +207,7 @@ describe("Gallery", () => {
const response = await junoClient.increaseNotebookViews(id); const response = await junoClient.increaseNotebookViews(id);
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith(`${configContext.JUNO_ENDPOINT}/api/notebooks/gallery/${id}/views`, { expect(window.fetch).toBeCalledWith(`${JunoClient.getJunoEndpoint()}/api/notebooks/gallery/${id}/views`, {
method: "PATCH", method: "PATCH",
}); });
}); });
@ -225,7 +224,9 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith( expect(window.fetch).toBeCalledWith(
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}/downloads`, `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
sampleDatabaseAccount.name
}/gallery/${id}/downloads`,
{ {
method: "PATCH", method: "PATCH",
headers: { headers: {
@ -248,7 +249,9 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith( expect(window.fetch).toBeCalledWith(
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}/favorite`, `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
sampleDatabaseAccount.name
}/gallery/${id}/favorite`,
{ {
method: "PATCH", method: "PATCH",
headers: { headers: {
@ -271,7 +274,9 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith( expect(window.fetch).toBeCalledWith(
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}/unfavorite`, `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
sampleDatabaseAccount.name
}/gallery/${id}/unfavorite`,
{ {
method: "PATCH", method: "PATCH",
headers: { headers: {
@ -293,7 +298,9 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith( expect(window.fetch).toBeCalledWith(
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/favorites`, `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
sampleDatabaseAccount.name
}/gallery/favorites`,
{ {
headers: { headers: {
[authorizationHeader.header]: authorizationHeader.token, [authorizationHeader.header]: authorizationHeader.token,
@ -314,7 +321,9 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith( expect(window.fetch).toBeCalledWith(
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/published`, `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
sampleDatabaseAccount.name
}/gallery/published`,
{ {
headers: { headers: {
[authorizationHeader.header]: authorizationHeader.token, [authorizationHeader.header]: authorizationHeader.token,
@ -336,7 +345,9 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith( expect(window.fetch).toBeCalledWith(
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery/${id}`, `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
sampleDatabaseAccount.name
}/gallery/${id}`,
{ {
method: "DELETE", method: "DELETE",
headers: { headers: {
@ -365,7 +376,9 @@ describe("Gallery", () => {
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(window.fetch).toBeCalledWith( expect(window.fetch).toBeCalledWith(
`${configContext.JUNO_ENDPOINT}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${sampleDatabaseAccount.name}/gallery`, `${JunoClient.getJunoEndpoint()}/api/notebooks/subscriptions/${sampleSubscriptionId}/databaseAccounts/${
sampleDatabaseAccount.name
}/gallery`,
{ {
method: "PUT", method: "PUT",
headers: { headers: {

View File

@ -7,7 +7,6 @@ import { IGitHubResponse } from "../GitHub/GitHubClient";
import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService"; import { IGitHubOAuthToken } from "../GitHub/GitHubOAuthService";
import { userContext } from "../UserContext"; import { userContext } from "../UserContext";
import { getAuthorizationHeader } from "../Utils/AuthorizationUtils"; import { getAuthorizationHeader } from "../Utils/AuthorizationUtils";
import { number } from "prop-types";
export interface IJunoResponse<T> { export interface IJunoResponse<T> {
status: number; status: number;
@ -484,8 +483,20 @@ export class JunoClient {
}; };
} }
// public for tests
public static getJunoEndpoint(): string {
const junoEndpoint = userContext.features.junoEndpoint ?? configContext.JUNO_ENDPOINT;
if (configContext.allowedJunoOrigins.indexOf(new URL(junoEndpoint).origin) === -1) {
const error = `${junoEndpoint} not allowed as juno endpoint`;
console.error(error);
throw new Error(error);
}
return junoEndpoint;
}
private getNotebooksUrl(): string { private getNotebooksUrl(): string {
return `${configContext.JUNO_ENDPOINT}/api/notebooks`; return `${JunoClient.getJunoEndpoint()}/api/notebooks`;
} }
private getAccount(): string { private getAccount(): string {
@ -501,7 +512,7 @@ export class JunoClient {
} }
private getAnalyticsUrl(): string { private getAnalyticsUrl(): string {
return `${configContext.JUNO_ENDPOINT}/api/analytics`; return `${JunoClient.getJunoEndpoint()}/api/analytics`;
} }
private static getHeaders(): HeadersInit { private static getHeaders(): HeadersInit {

View File

@ -13,6 +13,7 @@ export type Features = {
readonly enableTtl: boolean; readonly enableTtl: boolean;
readonly executeSproc: boolean; readonly executeSproc: boolean;
readonly hostedDataExplorer: boolean; readonly hostedDataExplorer: boolean;
readonly junoEndpoint?: string;
readonly livyEndpoint?: string; readonly livyEndpoint?: string;
readonly notebookBasePath?: string; readonly notebookBasePath?: string;
readonly notebookServerToken?: string; readonly notebookServerToken?: string;
@ -27,7 +28,8 @@ export type Features = {
export function extractFeatures(given = new URLSearchParams(window.location.search)): Features { export function extractFeatures(given = new URLSearchParams(window.location.search)): Features {
const downcased = new URLSearchParams(); const downcased = new URLSearchParams();
const set = (value: string, key: string) => downcased.set(key.toLowerCase(), value); const set = (value: string, key: string) => downcased.set(key.toLowerCase(), value);
const get = (key: string, defaultValue?: string) => downcased.get("feature." + key) ?? defaultValue; const get = (key: string, defaultValue?: string) =>
downcased.get("feature." + key) ?? downcased.get(key) ?? defaultValue;
try { try {
new URLSearchParams(window.parent.location.search).forEach(set); new URLSearchParams(window.parent.location.search).forEach(set);
@ -52,6 +54,7 @@ export function extractFeatures(given = new URLSearchParams(window.location.sear
enableTtl: "true" === get("enablettl"), enableTtl: "true" === get("enablettl"),
executeSproc: "true" === get("dataexplorerexecutesproc"), executeSproc: "true" === get("dataexplorerexecutesproc"),
hostedDataExplorer: "true" === get("hosteddataexplorerenabled"), hostedDataExplorer: "true" === get("hosteddataexplorerenabled"),
junoEndpoint: get("junoendpoint"),
livyEndpoint: get("livyendpoint"), livyEndpoint: get("livyendpoint"),
notebookBasePath: get("notebookbasepath"), notebookBasePath: get("notebookbasepath"),
notebookServerToken: get("notebookservertoken"), notebookServerToken: get("notebookservertoken"),