mirror of
https://github.com/OutlineFoundation/outline-server.git
synced 2026-08-04 06:35:06 +00:00
Repurposes TokenManager as CloudAccounts (#827)
* gitignore * Converts ManagedServerRepository into Account * Adds CloudAccounts * Moves token manager to cloud accounts * Auto-format * Newline * Oops wrong year * Addresses review commetns * Addresses review comments * Addresses review comments * Addresses review commetns
This commit is contained in:
parent
eb5ac5eeb4
commit
c36afbcbbb
10 changed files with 342 additions and 345 deletions
|
|
@ -89,11 +89,7 @@ export interface DigitalOceanSession {
|
|||
getDroplets(): Promise<DropletInfo[]>;
|
||||
}
|
||||
|
||||
export function createDigitalOceanSession(accessToken: string): DigitalOceanSession {
|
||||
return new RestApiSession(accessToken);
|
||||
}
|
||||
|
||||
class RestApiSession implements DigitalOceanSession {
|
||||
export class RestApiSession implements DigitalOceanSession {
|
||||
// Constructor takes a DigitalOcean access token, which should have
|
||||
// read+write permissions.
|
||||
constructor(public accessToken: string) {}
|
||||
|
|
|
|||
41
src/server_manager/model/digitalocean.ts
Normal file
41
src/server_manager/model/digitalocean.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2021 The Outline Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {ManagedServer, RegionId} from "./server";
|
||||
|
||||
// Keys are cityIds like "nyc". Values are regions like ["nyc1", "nyc3"].
|
||||
export type RegionMap = {
|
||||
[cityId: string]: RegionId[]
|
||||
};
|
||||
|
||||
export enum Status {
|
||||
ACTIVE,
|
||||
EMAIL_UNVERIFIED,
|
||||
MISSING_BILLING_INFORMATION,
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
// Returns the email address associated with the account.
|
||||
getName(): Promise<string>;
|
||||
// Returns the status of the account.
|
||||
getStatus(): Promise<Status>;
|
||||
// Lists all existing Shadowboxes. If `fetchFromHost` is true, performs a network request to
|
||||
// retrieve the servers; otherwise resolves with a cached server list.
|
||||
listServers(fetchFromHost?: boolean): Promise<ManagedServer[]>;
|
||||
// Return a map of regions that are available and support our target machine size.
|
||||
getRegionMap(): Promise<Readonly<RegionMap>>;
|
||||
// Creates a server and returning it when it becomes active (i.e. the server has
|
||||
// created, not necessarily once shadowbox installation has finished).
|
||||
createServer(region: RegionId, name: string): Promise<ManagedServer>;
|
||||
}
|
||||
|
|
@ -123,25 +123,6 @@ export class MonetaryCost {
|
|||
|
||||
export type RegionId = string;
|
||||
|
||||
// Keys are cityIds like "nyc". Values are regions like ["nyc1", "nyc3"].
|
||||
export type RegionMap = {
|
||||
[cityId: string]: RegionId[]
|
||||
};
|
||||
|
||||
// Repository of ManagedServer objects. These servers are created by the server
|
||||
// manager on cloud providers where we can provide a "magical" user experience,
|
||||
// e.g. DigitalOcean.
|
||||
export interface ManagedServerRepository {
|
||||
// Lists all existing Shadowboxes. If `fetchFromHost` is true, performs a network request to
|
||||
// retrieve the servers; otherwise resolves with a cached server list.
|
||||
listServers(fetchFromHost?: boolean): Promise<ManagedServer[]>;
|
||||
// Return a map of regions that are available and support our target machine size.
|
||||
getRegionMap(): Promise<Readonly<RegionMap>>;
|
||||
// Creates a server and returning it when it becomes active (i.e. the server has
|
||||
// created, not necessarily once shadowbox installation has finished).
|
||||
createServer(region: RegionId, name: string): Promise<ManagedServer>;
|
||||
}
|
||||
|
||||
// Configuration for manual servers. This is the output emitted from the
|
||||
// shadowbox install script, which is needed for the manager connect to
|
||||
// shadowbox.
|
||||
|
|
|
|||
|
|
@ -14,18 +14,15 @@
|
|||
|
||||
import './ui_components/app-root.js';
|
||||
|
||||
import * as digitalocean_api from '../cloud/digitalocean_api';
|
||||
import * as server from '../model/server';
|
||||
import * as digitalocean from "../model/digitalocean";
|
||||
|
||||
import {App, LAST_DISPLAYED_SERVER_STORAGE_KEY} from './app';
|
||||
import {TokenManager} from './digitalocean_oauth';
|
||||
import {AppRoot} from './ui_components/app-root';
|
||||
|
||||
const TOKEN_WITH_NO_SERVERS = 'no-server-token';
|
||||
const TOKEN_WITH_ONE_SERVER = 'one-server-token';
|
||||
import {CloudAccounts} from "./cloud_accounts";
|
||||
import {InMemoryStorage} from "../infrastructure/memory_storage";
|
||||
|
||||
// Define functions from preload.ts.
|
||||
|
||||
// tslint:disable-next-line:no-any
|
||||
(global as any).onUpdateDownloaded = () => {};
|
||||
// tslint:disable-next-line:no-any
|
||||
|
|
@ -39,7 +36,7 @@ beforeEach(() => {
|
|||
describe('App', () => {
|
||||
it('shows intro when starting with no manual servers or DigitalOcean token', async () => {
|
||||
const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
|
||||
const app = createTestApp(appRoot, new InMemoryDigitalOceanTokenManager());
|
||||
const app = createTestApp(appRoot);
|
||||
await app.start();
|
||||
expect(appRoot.currentPage).toEqual('intro');
|
||||
});
|
||||
|
|
@ -47,7 +44,7 @@ describe('App', () => {
|
|||
it('will not create a manual server with invalid input', async () => {
|
||||
// Create a new app with no existing servers or DigitalOcean token.
|
||||
const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
|
||||
const app = createTestApp(appRoot, new InMemoryDigitalOceanTokenManager());
|
||||
const app = createTestApp(appRoot);
|
||||
await app.start();
|
||||
expect(appRoot.currentPage).toEqual('intro');
|
||||
await expectAsync(app.createManualServer('bad input')).toBeRejectedWithError();
|
||||
|
|
@ -56,7 +53,7 @@ describe('App', () => {
|
|||
it('creates a manual server with valid input', async () => {
|
||||
// Create a new app with no existing servers or DigitalOcean token.
|
||||
const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
|
||||
const app = createTestApp(appRoot, new InMemoryDigitalOceanTokenManager());
|
||||
const app = createTestApp(appRoot);
|
||||
await app.start();
|
||||
expect(appRoot.currentPage).toEqual('intro');
|
||||
await app.createManualServer(JSON.stringify({certSha256: 'cert', apiUrl: 'url'}));
|
||||
|
|
@ -65,26 +62,30 @@ describe('App', () => {
|
|||
|
||||
it('initially shows servers', async () => {
|
||||
// Create fake servers and simulate their metadata being cached before creating the app.
|
||||
const tokenManager = new InMemoryDigitalOceanTokenManager();
|
||||
tokenManager.token = TOKEN_WITH_NO_SERVERS;
|
||||
const managedServerRepo = new FakeManagedServerRepository();
|
||||
const managedServer = await managedServerRepo.createServer('fake-managed-server-id');
|
||||
managedServer.apiUrl = 'fake-managed-server-api-url';
|
||||
const fakeAccount = new FakeDigitalOceanAccount();
|
||||
await fakeAccount.createServer('fake-managed-server-id');
|
||||
const cloudAccounts = makeCloudAccountsWithDoAccount(fakeAccount);
|
||||
|
||||
const manualServerRepo = new FakeManualServerRepository();
|
||||
await manualServerRepo.addServer({certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-1'});
|
||||
await manualServerRepo.addServer({certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-2'});
|
||||
|
||||
const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
|
||||
expect(appRoot.serverList.length).toEqual(0);
|
||||
const app = createTestApp(appRoot, tokenManager, manualServerRepo, managedServerRepo);
|
||||
const app = createTestApp(appRoot, cloudAccounts, manualServerRepo);
|
||||
|
||||
await app.start();
|
||||
// Validate that server metadata is shown.
|
||||
const managedServers = await managedServerRepo.listServers();
|
||||
const managedServers = await fakeAccount.listServers();
|
||||
expect(managedServers.length).toEqual(1);
|
||||
const manualServers = await manualServerRepo.listServers();
|
||||
expect(manualServers.length).toEqual(2);
|
||||
appRoot.getServerView('');
|
||||
const serverList = appRoot.serverList;
|
||||
|
||||
console.log(`managedServers.length: ${managedServers.length}`);
|
||||
console.log(`manualServers.length: ${manualServers.length}`);
|
||||
|
||||
expect(serverList.length).toEqual(manualServers.length + managedServers.length);
|
||||
expect(serverList).toContain(jasmine.objectContaining({id: 'fake-manual-server-api-url-1'}));
|
||||
expect(serverList).toContain(jasmine.objectContaining({id: 'fake-manual-server-api-url-2'}));
|
||||
|
|
@ -92,9 +93,6 @@ describe('App', () => {
|
|||
});
|
||||
|
||||
it('initially shows the last selected server', async () => {
|
||||
const tokenManager = new InMemoryDigitalOceanTokenManager();
|
||||
tokenManager.token = TOKEN_WITH_ONE_SERVER;
|
||||
|
||||
const LAST_DISPLAYED_SERVER_ID = 'fake-manual-server-api-url-1';
|
||||
const manualServerRepo = new FakeManualServerRepository();
|
||||
const lastDisplayedServer =
|
||||
|
|
@ -102,7 +100,7 @@ describe('App', () => {
|
|||
await manualServerRepo.addServer({certSha256: 'cert', apiUrl: 'fake-manual-server-api-url-2'});
|
||||
localStorage.setItem('lastDisplayedServer', LAST_DISPLAYED_SERVER_ID);
|
||||
const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
|
||||
const app = createTestApp(appRoot, tokenManager, manualServerRepo);
|
||||
const app = createTestApp(appRoot, null, manualServerRepo);
|
||||
await app.start();
|
||||
expect(appRoot.currentPage).toEqual('serverView');
|
||||
expect(appRoot.selectedServerId).toEqual(lastDisplayedServer.getManagementApiUrl());
|
||||
|
|
@ -111,9 +109,8 @@ describe('App', () => {
|
|||
it('shows progress screen once DigitalOcean droplets are created', async () => {
|
||||
// Start the app with a fake DigitalOcean token.
|
||||
const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
|
||||
const tokenManager = new InMemoryDigitalOceanTokenManager();
|
||||
tokenManager.token = TOKEN_WITH_NO_SERVERS;
|
||||
const app = createTestApp(appRoot, tokenManager);
|
||||
const cloudAccounts = makeCloudAccountsWithDoAccount(new FakeDigitalOceanAccount());
|
||||
const app = createTestApp(appRoot, cloudAccounts);
|
||||
await app.start();
|
||||
await app.createDigitalOceanServer('fakeRegion');
|
||||
expect(appRoot.currentPage).toEqual('serverView');
|
||||
|
|
@ -123,12 +120,10 @@ describe('App', () => {
|
|||
it('shows progress screen when starting with DigitalOcean servers still being created',
|
||||
async () => {
|
||||
const appRoot = document.getElementById('appRoot') as unknown as AppRoot;
|
||||
const tokenManager = new InMemoryDigitalOceanTokenManager();
|
||||
tokenManager.token = TOKEN_WITH_NO_SERVERS;
|
||||
const managedSeverRepository = new FakeManagedServerRepository();
|
||||
// Manually create the server since the DO repository server factory function is synchronous.
|
||||
const server = await managedSeverRepository.createServer();
|
||||
const app = createTestApp(appRoot, tokenManager, null, managedSeverRepository);
|
||||
const fakeAccount = new FakeDigitalOceanAccount();
|
||||
const server = await fakeAccount.createServer(Math.random().toString());
|
||||
const cloudAccounts = makeCloudAccountsWithDoAccount(fakeAccount);
|
||||
const app = createTestApp(appRoot, cloudAccounts, null);
|
||||
// Sets last displayed server.
|
||||
localStorage.setItem(LAST_DISPLAYED_SERVER_STORAGE_KEY, server.getId());
|
||||
await app.start();
|
||||
|
|
@ -137,28 +132,24 @@ describe('App', () => {
|
|||
});
|
||||
});
|
||||
|
||||
function makeCloudAccountsWithDoAccount(fakeAccount: FakeDigitalOceanAccount) {
|
||||
const fakeDigitalOceanAccountFactory = (token: string) => fakeAccount;
|
||||
const cloudAccounts = new CloudAccounts(fakeDigitalOceanAccountFactory, new InMemoryStorage());
|
||||
cloudAccounts.connectDigitalOceanAccount('fake-access-token');
|
||||
return cloudAccounts;
|
||||
}
|
||||
|
||||
function createTestApp(
|
||||
appRoot: AppRoot, digitalOceanTokenManager: InMemoryDigitalOceanTokenManager,
|
||||
manualServerRepo?: server.ManualServerRepository,
|
||||
managedServerRepository?: FakeManagedServerRepository) {
|
||||
appRoot: AppRoot, cloudAccounts?: CloudAccounts,
|
||||
manualServerRepo?: server.ManualServerRepository) {
|
||||
const VERSION = '0.0.1';
|
||||
const fakeDigitalOceanSessionFactory = (accessToken: string) => {
|
||||
return new FakeDigitalOceanSession(accessToken);
|
||||
};
|
||||
const fakeDigitalOceanServerRepositoryFactory =
|
||||
(session: digitalocean_api.DigitalOceanSession) => {
|
||||
const repo = managedServerRepository || new FakeManagedServerRepository();
|
||||
if (session.accessToken === TOKEN_WITH_ONE_SERVER) {
|
||||
repo.createServer(); // OK to ignore promise as the fake implementation is synchronous.
|
||||
}
|
||||
return repo;
|
||||
};
|
||||
if (!cloudAccounts) {
|
||||
cloudAccounts = new CloudAccounts((token: string) => new FakeDigitalOceanAccount(), new InMemoryStorage());
|
||||
}
|
||||
if (!manualServerRepo) {
|
||||
manualServerRepo = new FakeManualServerRepository();
|
||||
}
|
||||
return new App(
|
||||
appRoot, VERSION, fakeDigitalOceanSessionFactory, fakeDigitalOceanServerRepositoryFactory,
|
||||
manualServerRepo, digitalOceanTokenManager);
|
||||
return new App(appRoot, VERSION, manualServerRepo, cloudAccounts);
|
||||
}
|
||||
|
||||
class FakeServer implements server.Server {
|
||||
|
|
@ -272,46 +263,6 @@ class FakeManualServerRepository implements server.ManualServerRepository {
|
|||
}
|
||||
}
|
||||
|
||||
class InMemoryDigitalOceanTokenManager implements TokenManager {
|
||||
public token: string;
|
||||
getStoredToken(): string {
|
||||
return this.token;
|
||||
}
|
||||
removeTokenFromStorage() {
|
||||
this.token = null;
|
||||
}
|
||||
writeTokenToStorage(token: string) {
|
||||
this.token = token;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeDigitalOceanSession implements digitalocean_api.DigitalOceanSession {
|
||||
constructor(public accessToken: string) {}
|
||||
|
||||
// Return fake account data.
|
||||
getAccount() {
|
||||
return Promise.resolve(
|
||||
{email: 'fake@email.com', uuid: 'fake', email_verified: true, status: 'active'});
|
||||
}
|
||||
|
||||
// Return an empty list of droplets by default.
|
||||
getDropletsByTag = (tag: string) => Promise.resolve([]);
|
||||
|
||||
// Return an empty list of regions by default.
|
||||
getRegionInfo = () => Promise.resolve([]);
|
||||
|
||||
// Other methods do not yet need implementations for tests to pass.
|
||||
createDroplet =
|
||||
(displayName: string, region: string, publicKeyForSSH: string,
|
||||
dropletSpec: digitalocean_api.DigitalOceanDropletSpecification) =>
|
||||
Promise.reject(new Error('createDroplet not implemented'));
|
||||
deleteDroplet = (dropletId: number) => Promise.reject(new Error('deleteDroplet not implemented'));
|
||||
getDroplet = (dropletId: number) => Promise.reject(new Error('getDroplet not implemented'));
|
||||
getDropletTags = (dropletId: number) =>
|
||||
Promise.reject(new Error('getDropletTags not implemented'));
|
||||
getDroplets = () => Promise.reject(new Error('getDroplets not implemented'));
|
||||
}
|
||||
|
||||
class FakeManagedServer extends FakeServer implements server.ManagedServer {
|
||||
constructor(id: string, private isInstalled = true) {
|
||||
super(id);
|
||||
|
|
@ -335,8 +286,14 @@ class FakeManagedServer extends FakeServer implements server.ManagedServer {
|
|||
}
|
||||
}
|
||||
|
||||
class FakeManagedServerRepository implements server.ManagedServerRepository {
|
||||
class FakeDigitalOceanAccount implements digitalocean.Account {
|
||||
private servers: server.ManagedServer[] = [];
|
||||
async getName(): Promise<string> {
|
||||
return 'name';
|
||||
}
|
||||
async getStatus(): Promise<digitalocean.Status> {
|
||||
return digitalocean.Status.ACTIVE;
|
||||
}
|
||||
listServers() {
|
||||
return Promise.resolve(this.servers);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ import * as digitalocean_api from '../cloud/digitalocean_api';
|
|||
import * as errors from '../infrastructure/errors';
|
||||
import {sleep} from '../infrastructure/sleep';
|
||||
import * as server from '../model/server';
|
||||
import * as digitalocean from '../model/digitalocean';
|
||||
|
||||
import {formatBytes} from './data_formatting';
|
||||
import {TokenManager} from './digitalocean_oauth';
|
||||
import {CloudAccounts} from './cloud_accounts';
|
||||
import * as digitalocean_server from './digitalocean_server';
|
||||
import {parseManualServerConfig} from './management_urls';
|
||||
import {AppRoot, ServerListEntry} from './ui_components/app-root';
|
||||
|
|
@ -130,34 +130,24 @@ function isManualServer(testServer: server.Server): testServer is server.ManualS
|
|||
return !!(testServer as server.ManualServer).forget;
|
||||
}
|
||||
|
||||
function localizeDate(date: Date, language: string): string {
|
||||
return date.toLocaleString(language, {year: 'numeric', month: 'long', day: 'numeric'});
|
||||
}
|
||||
|
||||
type DigitalOceanSessionFactory = (accessToken: string) => digitalocean_api.DigitalOceanSession;
|
||||
type DigitalOceanServerRepositoryFactory = (session: digitalocean_api.DigitalOceanSession) =>
|
||||
server.ManagedServerRepository;
|
||||
|
||||
export class App {
|
||||
private digitalOceanRepository: server.ManagedServerRepository;
|
||||
private digitalOceanAccount: digitalocean.Account;
|
||||
private selectedServer: server.Server;
|
||||
private idServerMap = new Map<string, server.Server>();
|
||||
|
||||
constructor(
|
||||
private appRoot: AppRoot, private readonly version: string,
|
||||
private createDigitalOceanSession: DigitalOceanSessionFactory,
|
||||
private createDigitalOceanServerRepository: DigitalOceanServerRepositoryFactory,
|
||||
private manualServerRepository: server.ManualServerRepository,
|
||||
private digitalOceanTokenManager: TokenManager) {
|
||||
private cloudAccounts: CloudAccounts) {
|
||||
appRoot.setAttribute('outline-version', this.version);
|
||||
|
||||
appRoot.addEventListener('ConnectDigitalOceanAccountRequested', (event: CustomEvent) => {
|
||||
this.handleConnectDigitalOceanAccountRequest();
|
||||
});
|
||||
appRoot.addEventListener('CreateDigitalOceanServerRequested', (event: CustomEvent) => {
|
||||
const accessToken = this.digitalOceanTokenManager?.getStoredToken();
|
||||
if (accessToken) {
|
||||
this.showDigitalOceanCreateServer(accessToken);
|
||||
const digitalOceanAccount = this.cloudAccounts.getDigitalOceanAccount();
|
||||
if (digitalOceanAccount) {
|
||||
this.showDigitalOceanCreateServer(digitalOceanAccount);
|
||||
} else {
|
||||
console.error('Access token not found for server creation');
|
||||
this.handleConnectDigitalOceanAccountRequest();
|
||||
|
|
@ -325,9 +315,11 @@ export class App {
|
|||
async start(): Promise<void> {
|
||||
this.showIntro();
|
||||
|
||||
console.log('CloudAccounts', this.cloudAccounts.getDigitalOceanAccount());
|
||||
|
||||
// Load server list. Fetch manual and managed servers in parallel.
|
||||
await Promise.all([
|
||||
this.loadDigitalOceanServers(this.digitalOceanTokenManager?.getStoredToken()),
|
||||
this.loadDigitalOceanServers(this.cloudAccounts.getDigitalOceanAccount()),
|
||||
this.loadManualServers()
|
||||
]);
|
||||
|
||||
|
|
@ -341,19 +333,19 @@ export class App {
|
|||
}
|
||||
}
|
||||
|
||||
private async loadDigitalOceanServers(accessToken: string): Promise<server.ManagedServer[]> {
|
||||
if (!accessToken) {
|
||||
private async loadDigitalOceanServers(digitalOceanAccount: digitalocean.Account):
|
||||
Promise<server.ManagedServer[]> {
|
||||
if (!digitalOceanAccount) {
|
||||
return [];
|
||||
}
|
||||
try {
|
||||
const doSession = this.createDigitalOceanSession(accessToken);
|
||||
const doAccount = await doSession.getAccount();
|
||||
this.appRoot.adminEmail = doAccount.email;
|
||||
this.digitalOceanRepository = this.createDigitalOceanServerRepository(doSession);
|
||||
if (doAccount.status !== 'active') {
|
||||
this.digitalOceanAccount = digitalOceanAccount;
|
||||
this.appRoot.adminEmail = await this.digitalOceanAccount.getName();
|
||||
const status = await this.digitalOceanAccount.getStatus();
|
||||
if (status !== digitalocean.Status.ACTIVE) {
|
||||
return;
|
||||
}
|
||||
const servers = await this.digitalOceanRepository.listServers();
|
||||
const servers = await this.digitalOceanAccount.listServers();
|
||||
for (const server of servers) {
|
||||
this.addServer(server);
|
||||
}
|
||||
|
|
@ -445,8 +437,8 @@ export class App {
|
|||
|
||||
// Returns a promise that resolves when the account is active.
|
||||
// Throws CANCELLED_ERROR on cancellation, and the error on failure.
|
||||
private async ensureActiveDigitalOceanAccount(accessToken: string): Promise<void> {
|
||||
const doSession = this.createDigitalOceanSession(accessToken);
|
||||
private async ensureActiveDigitalOceanAccount(digitalOceanAccount: digitalocean.Account):
|
||||
Promise<void> {
|
||||
let cancelled = false;
|
||||
let activatingAccount = false;
|
||||
|
||||
|
|
@ -457,13 +449,13 @@ export class App {
|
|||
};
|
||||
const oauthUi = this.appRoot.getDigitalOceanOauthFlow(signOutAction);
|
||||
while (true) {
|
||||
const account = await this.digitalOceanRetry(async () => {
|
||||
const status = await this.digitalOceanRetry(async () => {
|
||||
if (cancelled) {
|
||||
throw CANCELLED_ERROR;
|
||||
}
|
||||
return await doSession.getAccount();
|
||||
return await digitalOceanAccount.getStatus();
|
||||
});
|
||||
if (account.status === 'active') {
|
||||
if (status === digitalocean.Status.ACTIVE) {
|
||||
bringToFront();
|
||||
if (activatingAccount) {
|
||||
// Show the 'account active' screen for a few seconds if the account was activated
|
||||
|
|
@ -475,7 +467,7 @@ export class App {
|
|||
}
|
||||
this.appRoot.showDigitalOceanOauthFlow();
|
||||
activatingAccount = true;
|
||||
if (account.email_verified) {
|
||||
if (status === digitalocean.Status.MISSING_BILLING_INFORMATION) {
|
||||
oauthUi.showBilling();
|
||||
} else {
|
||||
oauthUi.showEmailVerification();
|
||||
|
|
@ -531,16 +523,13 @@ export class App {
|
|||
};
|
||||
this.appRoot.getAndShowDigitalOceanOauthFlow(handleOauthFlowCancelled);
|
||||
try {
|
||||
const accessToken = await oauth.result;
|
||||
// Save accessToken to storage. DigitalOcean tokens
|
||||
// expire after 30 days, unless they are manually revoked by the user.
|
||||
// After 30 days the user will have to sign into DigitalOcean again.
|
||||
// Note we cannot yet use DigitalOcean refresh tokens, as they require
|
||||
// a client_secret to be stored on a server and not visible to end users
|
||||
// in client-side JS. More details at:
|
||||
// DigitalOcean tokens expire after 30 days, unless they are manually
|
||||
// revoked by the user. After 30 days the user will have to sign into
|
||||
// DigitalOcean again. Note we cannot yet use DigitalOcean refresh
|
||||
// tokens, as they require a client_secret to be stored on a server and
|
||||
// not visible to end users in client-side JS. More details at:
|
||||
// https://developers.digitalocean.com/documentation/oauth/#refresh-token-flow
|
||||
this.digitalOceanTokenManager.writeTokenToStorage(accessToken);
|
||||
return accessToken;
|
||||
return await oauth.result;
|
||||
} catch (error) {
|
||||
if (oauth.isCancelled()) {
|
||||
throw CANCELLED_ERROR;
|
||||
|
|
@ -551,9 +540,10 @@ export class App {
|
|||
}
|
||||
|
||||
private async handleConnectDigitalOceanAccountRequest(): Promise<void> {
|
||||
let accessToken: string;
|
||||
let digitalOceanAccount: digitalocean.Account;
|
||||
try {
|
||||
accessToken = await this.runDigitalOceanOauthFlow();
|
||||
const accessToken = await this.runDigitalOceanOauthFlow();
|
||||
digitalOceanAccount = this.cloudAccounts.connectDigitalOceanAccount(accessToken);
|
||||
} catch (error) {
|
||||
this.disconnectDigitalOceanAccount();
|
||||
this.showIntro();
|
||||
|
|
@ -564,18 +554,18 @@ export class App {
|
|||
}
|
||||
return;
|
||||
}
|
||||
const doServers = await this.loadDigitalOceanServers(accessToken);
|
||||
const doServers = await this.loadDigitalOceanServers(digitalOceanAccount);
|
||||
if (doServers.length > 0) {
|
||||
this.showServer(doServers[0]);
|
||||
} else {
|
||||
await this.showDigitalOceanCreateServer(accessToken);
|
||||
await this.showDigitalOceanCreateServer(digitalOceanAccount);
|
||||
}
|
||||
}
|
||||
|
||||
// Clears the credentials and returns to the intro screen.
|
||||
private disconnectDigitalOceanAccount(): void {
|
||||
this.digitalOceanTokenManager.removeTokenFromStorage();
|
||||
this.digitalOceanRepository = null;
|
||||
this.cloudAccounts.disconnectDigitalOceanAccount();
|
||||
this.digitalOceanAccount = null;
|
||||
for (const serverEntry of this.appRoot.serverList) {
|
||||
if (serverEntry.isManaged) {
|
||||
this.removeServer(serverEntry.id);
|
||||
|
|
@ -585,9 +575,10 @@ export class App {
|
|||
}
|
||||
|
||||
// Opens the screen to create a server.
|
||||
private async showDigitalOceanCreateServer(accessToken: string): Promise<void> {
|
||||
private async showDigitalOceanCreateServer(digitalOceanAccount: digitalocean.Account):
|
||||
Promise<void> {
|
||||
try {
|
||||
await this.ensureActiveDigitalOceanAccount(accessToken);
|
||||
await this.ensureActiveDigitalOceanAccount(digitalOceanAccount);
|
||||
} catch (error) {
|
||||
if (this.appRoot.currentPage === 'digitalOceanOauth') {
|
||||
this.showIntro();
|
||||
|
|
@ -604,7 +595,7 @@ export class App {
|
|||
try {
|
||||
const regionPicker = this.appRoot.getAndShowRegionPicker();
|
||||
const map = await this.digitalOceanRetry(() => {
|
||||
return this.digitalOceanRepository.getRegionMap();
|
||||
return this.digitalOceanAccount.getRegionMap();
|
||||
});
|
||||
const locations = Object.entries(map).map(([cityId, regionIds]) => {
|
||||
return this.createLocationModel(cityId, regionIds);
|
||||
|
|
@ -622,7 +613,7 @@ export class App {
|
|||
try {
|
||||
const serverName = this.makeLocalizedServerName(regionId);
|
||||
const server = await this.digitalOceanRetry(() => {
|
||||
return this.digitalOceanRepository.createServer(regionId, serverName);
|
||||
return this.digitalOceanAccount.createServer(regionId, serverName);
|
||||
});
|
||||
this.addServer(server);
|
||||
this.showServer(server);
|
||||
|
|
|
|||
50
src/server_manager/web_app/cloud_accounts.ts
Normal file
50
src/server_manager/web_app/cloud_accounts.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright 2021 The Outline Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {Account} from '../model/digitalocean';
|
||||
|
||||
type DigitalOceanAccountFactory = (accessToken: string) => Account;
|
||||
|
||||
export class CloudAccounts {
|
||||
private readonly DIGITALOCEAN_TOKEN_STORAGE_KEY = 'LastDOToken';
|
||||
|
||||
constructor(
|
||||
private digitalOceanAccountFactory: DigitalOceanAccountFactory,
|
||||
private storage = localStorage) {}
|
||||
|
||||
connectDigitalOceanAccount(token: string): Account {
|
||||
this.writeTokenToStorage(token);
|
||||
return this.getDigitalOceanAccount();
|
||||
}
|
||||
|
||||
disconnectDigitalOceanAccount(): void {
|
||||
this.storage.removeItem(this.DIGITALOCEAN_TOKEN_STORAGE_KEY);
|
||||
}
|
||||
|
||||
getDigitalOceanAccount(): Account {
|
||||
const token = this.getTokenFromStorage();
|
||||
if (token) {
|
||||
return this.digitalOceanAccountFactory(token);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private writeTokenToStorage(token: string): void {
|
||||
this.storage.setItem(this.DIGITALOCEAN_TOKEN_STORAGE_KEY, token);
|
||||
}
|
||||
|
||||
private getTokenFromStorage(): string {
|
||||
return this.storage.getItem(this.DIGITALOCEAN_TOKEN_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
148
src/server_manager/web_app/digitalocean_account.ts
Normal file
148
src/server_manager/web_app/digitalocean_account.ts
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
// Copyright 2021 The Outline Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import {DigitalOceanSession, DropletInfo} from "../cloud/digitalocean_api";
|
||||
import {DigitalOceanServer, GetCityId} from "./digitalocean_server";
|
||||
import * as server from "../model/server";
|
||||
import * as crypto from "../infrastructure/crypto";
|
||||
import * as digitalocean from "../model/digitalocean";
|
||||
import * as do_install_script from "../install_scripts/do_install_script";
|
||||
|
||||
// Tag used to mark Shadowbox Droplets.
|
||||
const SHADOWBOX_TAG = 'shadowbox';
|
||||
const MACHINE_SIZE = 's-1vcpu-1gb';
|
||||
|
||||
export interface ShadowboxSettings {
|
||||
imageId: string;
|
||||
metricsUrl: string;
|
||||
sentryApiUrl?: string;
|
||||
watchtowerRefreshSeconds?: number;
|
||||
}
|
||||
|
||||
export class DigitalOceanAccount implements digitalocean.Account {
|
||||
private servers: DigitalOceanServer[] = [];
|
||||
|
||||
constructor(
|
||||
private digitalOcean: DigitalOceanSession, private shadowboxSettings: ShadowboxSettings,
|
||||
private debugMode: boolean) {}
|
||||
|
||||
async getName(): Promise<string> {
|
||||
return (await this.digitalOcean.getAccount())?.email;
|
||||
}
|
||||
|
||||
async getStatus(): Promise<digitalocean.Status> {
|
||||
const account = await this.digitalOcean.getAccount();
|
||||
if (account.status === 'active') {
|
||||
return digitalocean.Status.ACTIVE;
|
||||
}
|
||||
if (!account.email_verified) {
|
||||
return digitalocean.Status.EMAIL_UNVERIFIED;
|
||||
}
|
||||
return digitalocean.Status.MISSING_BILLING_INFORMATION;
|
||||
}
|
||||
|
||||
// Return a map of regions that are available and support our target machine size.
|
||||
getRegionMap(): Promise<Readonly<digitalocean.RegionMap>> {
|
||||
return this.digitalOcean.getRegionInfo().then((regions) => {
|
||||
const ret: digitalocean.RegionMap = {};
|
||||
regions.forEach((region) => {
|
||||
const cityId = GetCityId(region.slug);
|
||||
if (!(cityId in ret)) {
|
||||
ret[cityId] = [];
|
||||
}
|
||||
if (region.available && region.sizes.indexOf(MACHINE_SIZE) !== -1) {
|
||||
ret[cityId].push(region.slug);
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
// Creates a server and returning it when it becomes active.
|
||||
createServer(region: server.RegionId, name: string): Promise<server.ManagedServer> {
|
||||
console.time('activeServer');
|
||||
console.time('servingServer');
|
||||
const onceKeyPair = crypto.generateKeyPair();
|
||||
const installCommand =
|
||||
getInstallScript(this.digitalOcean.accessToken, name, this.shadowboxSettings);
|
||||
|
||||
const dropletSpec = {
|
||||
installCommand,
|
||||
size: MACHINE_SIZE,
|
||||
image: 'docker-18-04',
|
||||
tags: [SHADOWBOX_TAG],
|
||||
};
|
||||
return onceKeyPair
|
||||
.then((keyPair) => {
|
||||
if (this.debugMode) {
|
||||
// Strip carriage returns, which produce weird blank lines when pasted into a terminal.
|
||||
console.debug(
|
||||
`private key for SSH access to new droplet:\n${
|
||||
keyPair.private.replace(/\r/g, '')}\n\n` +
|
||||
'Use "ssh -i keyfile root@[ip_address]" to connect to the machine');
|
||||
}
|
||||
return this.digitalOcean.createDroplet(name, region, keyPair.public, dropletSpec);
|
||||
})
|
||||
.then((response) => {
|
||||
return this.createDigitalOceanServer(this.digitalOcean, response.droplet);
|
||||
});
|
||||
}
|
||||
|
||||
listServers(fetchFromHost = true): Promise<server.ManagedServer[]> {
|
||||
if (!fetchFromHost) {
|
||||
return Promise.resolve(this.servers); // Return the in-memory servers.
|
||||
}
|
||||
return this.digitalOcean.getDropletsByTag(SHADOWBOX_TAG).then((droplets) => {
|
||||
this.servers = [];
|
||||
return droplets.map((droplet) => {
|
||||
return this.createDigitalOceanServer(this.digitalOcean, droplet);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Creates a DigitalOceanServer object and adds it to the in-memory server list.
|
||||
private createDigitalOceanServer(digitalOcean: DigitalOceanSession, dropletInfo: DropletInfo) {
|
||||
const server = new DigitalOceanServer(digitalOcean, dropletInfo);
|
||||
this.servers.push(server);
|
||||
return server;
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeDigitalOceanToken(input: string): string {
|
||||
const sanitizedInput = input.trim();
|
||||
const pattern = /^[A-Za-z0-9_\/-]+$/;
|
||||
if (!pattern.test(sanitizedInput)) {
|
||||
throw new Error('Invalid DigitalOcean Token');
|
||||
}
|
||||
return sanitizedInput;
|
||||
}
|
||||
|
||||
// cloudFunctions needs to define cloud::public_ip and cloud::add_tag.
|
||||
function getInstallScript(
|
||||
accessToken: string, name: string, shadowboxSettings: ShadowboxSettings): string {
|
||||
const sanitizedAccessToken = sanitizeDigitalOceanToken(accessToken);
|
||||
// TODO: consider shell escaping these variables.
|
||||
return '#!/bin/bash -eu\n' +
|
||||
`export DO_ACCESS_TOKEN=${sanitizedAccessToken}\n` +
|
||||
(shadowboxSettings.imageId ? `export SB_IMAGE=${shadowboxSettings.imageId}\n` : '') +
|
||||
(shadowboxSettings.watchtowerRefreshSeconds ?
|
||||
`export WATCHTOWER_REFRESH_SECONDS=${shadowboxSettings.watchtowerRefreshSeconds}\n` :
|
||||
'') +
|
||||
(shadowboxSettings.sentryApiUrl ?
|
||||
`export SENTRY_API_URL="${shadowboxSettings.sentryApiUrl}"\n` :
|
||||
'') +
|
||||
(shadowboxSettings.metricsUrl ? `export SB_METRICS_URL=${shadowboxSettings.metricsUrl}\n` :
|
||||
'') +
|
||||
`export SB_DEFAULT_SERVER_NAME="${name}"\n` + do_install_script.SCRIPT;
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
// Copyright 2018 The Outline Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
export interface TokenManager {
|
||||
// Returns the Oauth token, or null if unavailable.
|
||||
getStoredToken(): string;
|
||||
// Writes the token to storage.
|
||||
writeTokenToStorage(token: string): void;
|
||||
// Removes the token from storage.
|
||||
removeTokenFromStorage(): void;
|
||||
}
|
||||
|
||||
// TODO: this class combines URL manipulation with persistence logic.
|
||||
// Consider moving the URL manipulation logic to a separate class, so we
|
||||
// can pass in other implementations when the global "window" is not present.
|
||||
export class DigitalOceanTokenManager implements TokenManager {
|
||||
private readonly DIGITALOCEAN_TOKEN_STORAGE_KEY = 'LastDOToken';
|
||||
|
||||
// Searches the current URL (post-OAuth) and local storage for a DigitalOcean
|
||||
// access token. The token is not checked for validity as this would require
|
||||
// an extra roundtrip to DigitalOcean.
|
||||
getStoredToken(): string {
|
||||
const tokenFromStorage = this.getTokenFromStorage();
|
||||
if (tokenFromStorage) {
|
||||
console.info('found access token in local storage');
|
||||
return tokenFromStorage;
|
||||
}
|
||||
|
||||
// Not an error as user may not yet have authenticated.
|
||||
return null;
|
||||
}
|
||||
|
||||
writeTokenToStorage(token: string): void {
|
||||
localStorage.setItem(this.DIGITALOCEAN_TOKEN_STORAGE_KEY, token);
|
||||
}
|
||||
|
||||
removeTokenFromStorage(): void {
|
||||
localStorage.removeItem(this.DIGITALOCEAN_TOKEN_STORAGE_KEY);
|
||||
}
|
||||
|
||||
private getTokenFromStorage(): string {
|
||||
return localStorage.getItem(this.DIGITALOCEAN_TOKEN_STORAGE_KEY);
|
||||
}
|
||||
}
|
||||
|
|
@ -15,22 +15,14 @@
|
|||
import {EventEmitter} from 'eventemitter3';
|
||||
|
||||
import {DigitalOceanSession, DropletInfo} from '../cloud/digitalocean_api';
|
||||
import * as crypto from '../infrastructure/crypto';
|
||||
import * as errors from '../infrastructure/errors';
|
||||
import {asciiToHex, hexToString} from '../infrastructure/hex_encoding';
|
||||
import * as do_install_script from '../install_scripts/do_install_script';
|
||||
import * as server from '../model/server';
|
||||
|
||||
import {ShadowboxServer} from './shadowbox_server';
|
||||
|
||||
// WARNING: these strings must be lowercase due to a DigitalOcean case
|
||||
// sensitivity bug.
|
||||
|
||||
// Tag used to mark Shadowbox Droplets.
|
||||
const SHADOWBOX_TAG = 'shadowbox';
|
||||
// Prefix used in key-value tags.
|
||||
const KEY_VALUE_TAG = 'kv';
|
||||
|
||||
// The tag key for the manager API certificate fingerprint.
|
||||
const CERTIFICATE_FINGERPRINT_TAG = 'certsha256';
|
||||
// The tag key for the manager API URL.
|
||||
|
|
@ -64,7 +56,7 @@ enum InstallState {
|
|||
DELETED
|
||||
}
|
||||
|
||||
class DigitaloceanServer extends ShadowboxServer implements server.ManagedServer {
|
||||
export class DigitalOceanServer extends ShadowboxServer implements server.ManagedServer {
|
||||
private eventQueue = new EventEmitter();
|
||||
private installState: InstallState = InstallState.UNKNOWN;
|
||||
|
||||
|
|
@ -327,107 +319,3 @@ function startsWithCaseInsensitive(text: string, prefix: string) {
|
|||
export function GetCityId(slug: server.RegionId): string {
|
||||
return slug.substr(0, 3).toLowerCase();
|
||||
}
|
||||
|
||||
const MACHINE_SIZE = 's-1vcpu-1gb';
|
||||
|
||||
export class DigitaloceanServerRepository implements server.ManagedServerRepository {
|
||||
private servers: DigitaloceanServer[] = [];
|
||||
|
||||
constructor(
|
||||
private digitalOcean: DigitalOceanSession, private image: string, private metricsUrl: string,
|
||||
private sentryApiUrl: string|undefined, private debugMode: boolean) {}
|
||||
|
||||
// Return a map of regions that are available and support our target machine size.
|
||||
getRegionMap(): Promise<Readonly<server.RegionMap>> {
|
||||
return this.digitalOcean.getRegionInfo().then((regions) => {
|
||||
const ret: server.RegionMap = {};
|
||||
regions.forEach((region) => {
|
||||
const cityId = GetCityId(region.slug);
|
||||
if (!(cityId in ret)) {
|
||||
ret[cityId] = [];
|
||||
}
|
||||
if (region.available && region.sizes.indexOf(MACHINE_SIZE) !== -1) {
|
||||
ret[cityId].push(region.slug);
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
// Creates a server and returning it when it becomes active.
|
||||
createServer(region: server.RegionId, name: string): Promise<server.ManagedServer> {
|
||||
console.time('activeServer');
|
||||
console.time('servingServer');
|
||||
const onceKeyPair = crypto.generateKeyPair();
|
||||
const watchtowerRefreshSeconds = this.image ? 30 : undefined;
|
||||
const installCommand = getInstallScript(
|
||||
this.digitalOcean.accessToken, name, this.image, watchtowerRefreshSeconds, this.metricsUrl,
|
||||
this.sentryApiUrl);
|
||||
|
||||
const dropletSpec = {
|
||||
installCommand,
|
||||
size: MACHINE_SIZE,
|
||||
image: 'docker-18-04',
|
||||
tags: [SHADOWBOX_TAG],
|
||||
};
|
||||
return onceKeyPair
|
||||
.then((keyPair) => {
|
||||
if (this.debugMode) {
|
||||
// Strip carriage returns, which produce weird blank lines when pasted into a terminal.
|
||||
console.debug(
|
||||
`private key for SSH access to new droplet:\n${
|
||||
keyPair.private.replace(/\r/g, '')}\n\n` +
|
||||
'Use "ssh -i keyfile root@[ip_address]" to connect to the machine');
|
||||
}
|
||||
return this.digitalOcean.createDroplet(name, region, keyPair.public, dropletSpec);
|
||||
})
|
||||
.then((response) => {
|
||||
return this.createDigitalOceanServer(this.digitalOcean, response.droplet);
|
||||
});
|
||||
}
|
||||
|
||||
listServers(fetchFromHost = true): Promise<server.ManagedServer[]> {
|
||||
if (!fetchFromHost) {
|
||||
return Promise.resolve(this.servers); // Return the in-memory servers.
|
||||
}
|
||||
return this.digitalOcean.getDropletsByTag(SHADOWBOX_TAG).then((droplets) => {
|
||||
this.servers = [];
|
||||
return droplets.map((droplet) => {
|
||||
return this.createDigitalOceanServer(this.digitalOcean, droplet);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Creates a DigitaloceanServer object and adds it to the in-memory server list.
|
||||
private createDigitalOceanServer(digitalOcean: DigitalOceanSession, dropletInfo: DropletInfo) {
|
||||
const server = new DigitaloceanServer(digitalOcean, dropletInfo);
|
||||
this.servers.push(server);
|
||||
return server;
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeDigitaloceanToken(input: string): string {
|
||||
const sanitizedInput = input.trim();
|
||||
const pattern = /^[A-Za-z0-9_\/-]+$/;
|
||||
if (!pattern.test(sanitizedInput)) {
|
||||
throw new Error('Invalid DigitalOcean Token');
|
||||
}
|
||||
return sanitizedInput;
|
||||
}
|
||||
|
||||
// cloudFunctions needs to define cloud::public_ip and cloud::add_tag.
|
||||
function getInstallScript(
|
||||
accessToken: string, name: string, image?: string, watchtowerRefreshSeconds?: number,
|
||||
metricsUrl?: string, sentryApiUrl?: string): string {
|
||||
const sanitizedAccessToken = sanitizeDigitaloceanToken(accessToken);
|
||||
// TODO: consider shell escaping these variables.
|
||||
return '#!/bin/bash -eu\n' +
|
||||
`export DO_ACCESS_TOKEN=${sanitizedAccessToken}\n` +
|
||||
(image ? `export SB_IMAGE=${image}\n` : '') +
|
||||
(watchtowerRefreshSeconds ?
|
||||
`export WATCHTOWER_REFRESH_SECONDS=${watchtowerRefreshSeconds}\n` :
|
||||
'') +
|
||||
(sentryApiUrl ? `export SENTRY_API_URL="${sentryApiUrl}"\n` : '') +
|
||||
(metricsUrl ? `export SB_METRICS_URL=${metricsUrl}\n` : '') +
|
||||
`export SB_DEFAULT_SERVER_NAME="${name}"\n` + do_install_script.SCRIPT;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,11 +16,10 @@ import './ui_components/app-root.js';
|
|||
|
||||
import * as digitalocean_api from '../cloud/digitalocean_api';
|
||||
import * as i18n from '../infrastructure/i18n';
|
||||
import {getSentryApiUrl} from '../infrastructure/sentry';
|
||||
|
||||
import {App} from './app';
|
||||
import {DigitalOceanTokenManager} from './digitalocean_oauth';
|
||||
import * as digitalocean_server from './digitalocean_server';
|
||||
import {CloudAccounts} from './cloud_accounts';
|
||||
import {DigitalOceanAccount} from './digitalocean_account';
|
||||
import {ManualServerRepository} from './manual_server';
|
||||
import {AppRoot} from './ui_components/app-root.js';
|
||||
|
||||
|
|
@ -97,16 +96,22 @@ document.addEventListener('WebComponentsReady', () => {
|
|||
// Parse URL query params.
|
||||
const params = new URL(document.URL).searchParams;
|
||||
const debugMode = params.get('outlineDebugMode') === 'true';
|
||||
const metricsUrl = params.get('metricsUrl');
|
||||
const shadowboxImage = params.get('image');
|
||||
const version = params.get('version');
|
||||
const sentryDsn = params.get('sentryDsn');
|
||||
|
||||
const shadowboxImageId = params.get('image');
|
||||
const shadowboxSettings = {
|
||||
imageId: shadowboxImageId,
|
||||
metricsUrl: params.get('metricsUrl'),
|
||||
sentryApiUrl: params.get('sentryDsn'),
|
||||
watchtowerRefreshSeconds: shadowboxImageId ? 30 : undefined,
|
||||
};
|
||||
|
||||
// Set DigitalOcean server repository parameters.
|
||||
const digitalOceanServerRepositoryFactory = (session: digitalocean_api.DigitalOceanSession) => {
|
||||
return new digitalocean_server.DigitaloceanServerRepository(
|
||||
session, shadowboxImage, metricsUrl, getSentryApiUrl(sentryDsn), debugMode);
|
||||
const digitalOceanAccountFactory = (accessToken: string) => {
|
||||
const session = new digitalocean_api.RestApiSession(accessToken);
|
||||
return new DigitalOceanAccount(session, shadowboxSettings, debugMode);
|
||||
};
|
||||
const cloudAccounts = new CloudAccounts(digitalOceanAccountFactory);
|
||||
|
||||
// Create and start the app.
|
||||
const language = getLanguageToUse();
|
||||
|
|
@ -120,10 +125,5 @@ document.addEventListener('WebComponentsReady', () => {
|
|||
const filteredLanguageDefs = Object.values(SUPPORTED_LANGUAGES);
|
||||
appRoot.supportedLanguages = sortLanguageDefsByName(filteredLanguageDefs);
|
||||
appRoot.setLanguage(language.string(), languageDirection);
|
||||
new App(
|
||||
appRoot, version, digitalocean_api.createDigitalOceanSession,
|
||||
digitalOceanServerRepositoryFactory, new ManualServerRepository('manualServers'),
|
||||
new DigitalOceanTokenManager())
|
||||
.start();
|
||||
new App(appRoot, version, new ManualServerRepository('manualServers'), cloudAccounts).start();
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue