mirror of
https://github.com/OutlineFoundation/outline-server.git
synced 2026-08-04 14:37:34 +00:00
Rename states and listener pattern
This commit is contained in:
parent
b4e5981aa5
commit
fbc62f43b6
7 changed files with 63 additions and 66 deletions
|
|
@ -117,7 +117,7 @@ function cloud::add_encoded_kv_tag() {
|
|||
cloud::add_tag "kv:${key}:${value}"
|
||||
}
|
||||
|
||||
echo "true" | cloud::add_encoded_kv_tag "outline"
|
||||
echo "true" | cloud::add_encoded_kv_tag "install-started"
|
||||
|
||||
log_for_sentry "Starting install"
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export SHADOWBOX_DIR="${SHADOWBOX_DIR:-${HOME:-/root}/shadowbox}"
|
|||
mkdir -p "${SHADOWBOX_DIR}"
|
||||
|
||||
# Save output for debugging
|
||||
true > "${SHADOWBOX_DIR}/install-shadowbox-output"
|
||||
exec &> "${SHADOWBOX_DIR}/install-shadowbox-output"
|
||||
|
||||
function cloud::public_ip() {
|
||||
curl curl -H "Metadata-Flavor: Google" "http://metadata.google.internal/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip"
|
||||
|
|
@ -66,7 +66,7 @@ function cloud::set_guest_attribute() {
|
|||
curl -H "Metadata-Flavor: Google" -X PUT -d "${label_value}" "${SET_GUEST_ATTRIBUTE_URL}"
|
||||
}
|
||||
|
||||
cloud::set_guest_attribute "outline" "true"
|
||||
cloud::set_guest_attribute "install-started" "true"
|
||||
|
||||
# Enable BBR.
|
||||
# Recent DigitalOcean one-click images are based on Ubuntu 18 and have kernel 4.15+.
|
||||
|
|
|
|||
|
|
@ -107,10 +107,12 @@ export interface ManualServer extends Server {
|
|||
// Managed servers are servers created by the Outline Manager through our
|
||||
// "magic" user experience, e.g. DigitalOcean.
|
||||
export interface ManagedServer extends Server {
|
||||
// Indicates how far installation has progress.
|
||||
installProgress(): number;
|
||||
// Enables notifications related to installation progress.
|
||||
onInstallProgressChange: (progress: number) => void;
|
||||
// Returns a promise that fulfills once installation is complete.
|
||||
waitOnInstall(): Promise<void>;
|
||||
// Enables notifications related to installation progress.
|
||||
setProgressListener(listener: (progress: number) => void): void;
|
||||
// Returns server host object.
|
||||
getHost(): ManagedServerHost;
|
||||
// Returns true when installation is complete.
|
||||
|
|
|
|||
|
|
@ -802,9 +802,8 @@ export class App {
|
|||
const view = await this.appRoot.getServerView(server.getId());
|
||||
view.serverName = this.makeDisplayName(server);
|
||||
view.selectedPage = 'progressView';
|
||||
server.setProgressListener(progress => {
|
||||
view.installProgress = progress;
|
||||
});
|
||||
view.installProgress = server.installProgress();
|
||||
server.onInstallProgressChange = progress => view.installProgress = progress;
|
||||
}
|
||||
|
||||
private showMetricsOptInWhenNeeded(selectedServer: server.Server, serverView: ServerView) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import {ShadowboxServer} from './shadowbox_server';
|
|||
|
||||
// Prefix used in key-value tags.
|
||||
const KEY_VALUE_TAG = 'kv';
|
||||
// The tag that appears at the beginning of installation.
|
||||
const INSTALL_STARTED_TAG = 'install-started';
|
||||
// The tag key for the manager API certificate fingerprint.
|
||||
const CERTIFICATE_FINGERPRINT_TAG = 'certsha256';
|
||||
// The tag key for the manager API URL.
|
||||
|
|
@ -41,14 +43,14 @@ const DEPRECATED_API_PREFIX_TAG = 'apiprefix';
|
|||
enum InstallState {
|
||||
// Unknown state - server may still be installing.
|
||||
UNKNOWN = 0,
|
||||
// Server status is "active"
|
||||
CREATED,
|
||||
// Droplet status is "active"
|
||||
DROPLET_CREATED,
|
||||
// Userspace is running (detected by the presence of tags)
|
||||
BOOTED,
|
||||
DROPLET_RUNNING,
|
||||
// The server has generated its management service certificate.
|
||||
HAS_CERTIFICATE,
|
||||
CERTIFICATE_CREATED,
|
||||
// Server is running and has the API URL and certificate fingerprint set.
|
||||
SUCCESS,
|
||||
COMPLETED,
|
||||
// Server is in an error state.
|
||||
ERROR,
|
||||
// Server has been deleted.
|
||||
|
|
@ -58,7 +60,8 @@ enum InstallState {
|
|||
export class DigitalOceanServer extends ShadowboxServer implements server.ManagedServer {
|
||||
private eventQueue = new EventEmitter();
|
||||
private installState: InstallState = InstallState.UNKNOWN;
|
||||
private listener: (progress: number) => void;
|
||||
|
||||
public onInstallProgressChange: (progress: number) => void = null;
|
||||
|
||||
constructor(
|
||||
id: string, private digitalOcean: DigitalOceanSession, private dropletInfo: DropletInfo) {
|
||||
|
|
@ -81,7 +84,7 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
}
|
||||
// Final state is now known, so we can stop checking.
|
||||
clearInterval(intervalId);
|
||||
if (this.installState === InstallState.SUCCESS) {
|
||||
if (this.installState === InstallState.COMPLETED) {
|
||||
fulfill();
|
||||
} else if (this.installState === InstallState.ERROR) {
|
||||
reject(new errors.ServerInstallFailedError());
|
||||
|
|
@ -106,8 +109,8 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
return;
|
||||
}
|
||||
const tagMap = this.getTagMap();
|
||||
if (tagMap[INSTALL_ERROR_TAG]) {
|
||||
console.error(`error tag: ${tagMap[INSTALL_ERROR_TAG]}`);
|
||||
if (tagMap.get(INSTALL_ERROR_TAG)) {
|
||||
console.error(`error tag: ${tagMap.get(INSTALL_ERROR_TAG)}`);
|
||||
this.setInstallState(InstallState.ERROR);
|
||||
} else if (Date.now() - startTimestamp >= TIMEOUT_MS) {
|
||||
console.error('hit timeout while waiting for installation');
|
||||
|
|
@ -116,13 +119,13 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
// API Url and Certificate have been set, so we have successfully
|
||||
// installed the server and can now make API calls.
|
||||
console.info('digitalocean_server: Successfully found API and cert tags');
|
||||
this.setInstallState(InstallState.SUCCESS);
|
||||
} else if (tagMap[CERTIFICATE_FINGERPRINT_TAG]) {
|
||||
this.setInstallState(InstallState.HAS_CERTIFICATE);
|
||||
} else if (Object.keys(tagMap).length > 0) {
|
||||
this.setInstallState(InstallState.BOOTED);
|
||||
this.setInstallState(InstallState.COMPLETED);
|
||||
} else if (tagMap.get(CERTIFICATE_FINGERPRINT_TAG)) {
|
||||
this.setInstallState(InstallState.CERTIFICATE_CREATED);
|
||||
} else if (tagMap.get(INSTALL_STARTED_TAG)) {
|
||||
this.setInstallState(InstallState.DROPLET_RUNNING);
|
||||
} else if (this.dropletInfo?.status === 'active') {
|
||||
this.setInstallState(InstallState.CREATED);
|
||||
this.setInstallState(InstallState.DROPLET_CREATED);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -162,40 +165,35 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
}, 3000);
|
||||
}
|
||||
|
||||
setProgressListener(listener: (progress: number) => void): void {
|
||||
this.listener = listener;
|
||||
listener(this.installProgress());
|
||||
}
|
||||
|
||||
private setInstallState(installState: InstallState) {
|
||||
if (this.isInstallStateFinal()) {
|
||||
// The install state is final and cannot be changed.
|
||||
return;
|
||||
}
|
||||
this.installState = installState;
|
||||
if (this.installState === InstallState.SUCCESS) {
|
||||
if (this.installState === InstallState.COMPLETED) {
|
||||
this.setInstallCompleted();
|
||||
}
|
||||
if (this.listener) {
|
||||
this.listener(this.installProgress());
|
||||
if (this.onInstallProgressChange) {
|
||||
this.onInstallProgressChange(this.installProgress());
|
||||
}
|
||||
}
|
||||
|
||||
private installProgress(): number {
|
||||
public installProgress(): number {
|
||||
// Values are based on observed installation timing.
|
||||
// Installation typically takes 90 seconds in total.
|
||||
switch (this.installState) {
|
||||
case InstallState.UNKNOWN: return 0.1;
|
||||
case InstallState.CREATED: return 0.5;
|
||||
case InstallState.BOOTED: return 0.55;
|
||||
case InstallState.HAS_CERTIFICATE: return 0.6;
|
||||
case InstallState.SUCCESS: return 1.0;
|
||||
case InstallState.DROPLET_CREATED: return 0.5;
|
||||
case InstallState.DROPLET_RUNNING: return 0.55;
|
||||
case InstallState.CERTIFICATE_CREATED: return 0.6;
|
||||
case InstallState.COMPLETED: return 1.0;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private isInstallStateFinal(): boolean {
|
||||
return this.installState === InstallState.SUCCESS ||
|
||||
return this.installState === InstallState.COMPLETED ||
|
||||
this.installState === InstallState.ERROR ||
|
||||
this.installState === InstallState.DELETED;
|
||||
}
|
||||
|
|
@ -230,8 +228,8 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
}
|
||||
|
||||
// Gets the key-value map stored in the DigitalOcean tags.
|
||||
private getTagMap(): {[key: string]: string} {
|
||||
const ret: {[key: string]: string} = {};
|
||||
private getTagMap(): Map<string, string> {
|
||||
const ret = new Map<string, string>();
|
||||
const tagPrefix = KEY_VALUE_TAG + ':';
|
||||
for (const tag of this.dropletInfo.tags) {
|
||||
if (!startsWithCaseInsensitive(tag, tagPrefix)) {
|
||||
|
|
@ -240,7 +238,7 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
const keyValuePair = tag.slice(tagPrefix.length);
|
||||
const [key, hexValue] = keyValuePair.split(':', 2);
|
||||
try {
|
||||
ret[key.toLowerCase()] = hexToString(hexValue);
|
||||
ret.set(key.toLowerCase(), hexToString(hexValue));
|
||||
} catch (e) {
|
||||
console.error('error decoding hex string');
|
||||
}
|
||||
|
|
@ -261,11 +259,11 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
// Gets the address for the user management api, throws an error if unavailable.
|
||||
private getManagementApiAddress(): string {
|
||||
const tagMap = this.getTagMap();
|
||||
let apiAddress = tagMap[API_URL_TAG];
|
||||
let apiAddress = tagMap.get(API_URL_TAG);
|
||||
// Check the old tags for backward-compatibility.
|
||||
// TODO(fortuna): Delete this before we release v1.0
|
||||
if (!apiAddress) {
|
||||
const portNumber = tagMap[DEPRECATED_API_PORT_TAG];
|
||||
const portNumber = tagMap.get(DEPRECATED_API_PORT_TAG);
|
||||
if (!portNumber) {
|
||||
throw new Error('Could not get API port number');
|
||||
}
|
||||
|
|
@ -273,7 +271,7 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
throw new Error('API hostname not set');
|
||||
}
|
||||
apiAddress = `https://${this.ipv4Address()}:${portNumber}/`;
|
||||
const apiPrefix = tagMap[DEPRECATED_API_PREFIX_TAG];
|
||||
const apiPrefix = tagMap.get(DEPRECATED_API_PREFIX_TAG);
|
||||
if (apiPrefix) {
|
||||
apiAddress += apiPrefix + '/';
|
||||
}
|
||||
|
|
@ -287,7 +285,7 @@ export class DigitalOceanServer extends ShadowboxServer implements server.Manage
|
|||
// Gets the certificate fingerprint in base64 format, throws an error if
|
||||
// unavailable.
|
||||
private getCertificateFingerprint(): string {
|
||||
const fingerprint = this.getTagMap()[CERTIFICATE_FINGERPRINT_TAG];
|
||||
const fingerprint = this.getTagMap().get(CERTIFICATE_FINGERPRINT_TAG);
|
||||
if (fingerprint) {
|
||||
return btoa(fingerprint);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -29,11 +29,11 @@ enum InstallState {
|
|||
// The static IP has been allocated.
|
||||
IP_ALLOCATED,
|
||||
// The system has booted (detected by the creation of guest tags)
|
||||
BOOTED,
|
||||
INSTANCE_RUNNING,
|
||||
// The server has generated its management service certificate.
|
||||
HAS_CERTIFICATE,
|
||||
CERTIFICATE_CREATED,
|
||||
// Server is running and has the API URL and certificate fingerprint set.
|
||||
SUCCESS,
|
||||
COMPLETED,
|
||||
// Server is in an error state.
|
||||
ERROR,
|
||||
// Server deletion has been initiated.
|
||||
|
|
@ -48,7 +48,8 @@ export class GcpServer extends ShadowboxServer implements server.ManagedServer {
|
|||
private readonly instanceReadiness: Promise<void>;
|
||||
private readonly gcpHost: GcpHost;
|
||||
private installState: InstallState = InstallState.UNKNOWN;
|
||||
private listener: (progress: number) => void = null;
|
||||
|
||||
public onInstallProgressChange: (progress: number) => void = null;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
|
|
@ -115,7 +116,7 @@ export class GcpServer extends ShadowboxServer implements server.ManagedServer {
|
|||
}
|
||||
|
||||
isInstallCompleted(): boolean {
|
||||
return this.installState === InstallState.SUCCESS ||
|
||||
return this.installState === InstallState.COMPLETED ||
|
||||
this.installState === InstallState.ERROR ||
|
||||
this.installState === InstallState.DELETED;
|
||||
}
|
||||
|
|
@ -129,15 +130,15 @@ export class GcpServer extends ShadowboxServer implements server.ManagedServer {
|
|||
const apiUrl = outlineGuestAttributes.get('apiUrl');
|
||||
trustCertificate(certSha256);
|
||||
this.setManagementApiUrl(apiUrl);
|
||||
this.setInstallState(InstallState.SUCCESS);
|
||||
this.setInstallState(InstallState.COMPLETED);
|
||||
break;
|
||||
} else if (outlineGuestAttributes.has('install-error')) {
|
||||
this.setInstallState(InstallState.ERROR);
|
||||
break;
|
||||
} else if (outlineGuestAttributes.has('certSha256')) {
|
||||
this.setInstallState(InstallState.HAS_CERTIFICATE);
|
||||
} else if (outlineGuestAttributes.has('outline')) {
|
||||
this.setInstallState(InstallState.BOOTED);
|
||||
this.setInstallState(InstallState.CERTIFICATE_CREATED);
|
||||
} else if (outlineGuestAttributes.has('install-started')) {
|
||||
this.setInstallState(InstallState.INSTANCE_RUNNING);
|
||||
}
|
||||
|
||||
await sleep(GcpServer.GUEST_ATTRIBUTES_POLLING_INTERVAL_MS);
|
||||
|
|
@ -151,21 +152,16 @@ export class GcpServer extends ShadowboxServer implements server.ManagedServer {
|
|||
}
|
||||
}
|
||||
|
||||
setProgressListener(listener: (progress: number) => void): void {
|
||||
this.listener = listener;
|
||||
listener(this.installProgress());
|
||||
}
|
||||
|
||||
private installProgress(): number {
|
||||
public installProgress(): number {
|
||||
// Values are based on observed installation timing.
|
||||
// Installation typically takes ~5 minutes in total.
|
||||
switch (this.installState) {
|
||||
case InstallState.UNKNOWN: return 0.005;
|
||||
case InstallState.INSTANCE_CREATED: return 0.03;
|
||||
case InstallState.IP_ALLOCATED: return 0.04;
|
||||
case InstallState.BOOTED: return 0.2;
|
||||
case InstallState.HAS_CERTIFICATE: return 0.8;
|
||||
case InstallState.SUCCESS: return 1.0;
|
||||
case InstallState.INSTANCE_RUNNING: return 0.2;
|
||||
case InstallState.CERTIFICATE_CREATED: return 0.8;
|
||||
case InstallState.COMPLETED: return 1.0;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -183,8 +179,8 @@ export class GcpServer extends ShadowboxServer implements server.ManagedServer {
|
|||
|
||||
setInstallState(newState: InstallState): void {
|
||||
this.installState = newState;
|
||||
if (this.listener) {
|
||||
this.listener(this.installProgress());
|
||||
if (this.onInstallProgressChange) {
|
||||
this.onInstallProgressChange(this.installProgress());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,6 +214,8 @@ export class FakeManualServerRepository implements server.ManualServerRepository
|
|||
}
|
||||
|
||||
export class FakeManagedServer extends FakeServer implements server.ManagedServer {
|
||||
public onInstallProgressChange: (progress: number) => void = null;
|
||||
|
||||
constructor(id: string, private isInstalled = true) {
|
||||
super(id);
|
||||
}
|
||||
|
|
@ -222,8 +224,8 @@ export class FakeManagedServer extends FakeServer implements server.ManagedServe
|
|||
// shadowbox install time.
|
||||
return new Promise<void>((fulfill, reject) => {});
|
||||
}
|
||||
setProgressListener(listener: (progress: number) => void): void {
|
||||
listener(0.5);
|
||||
installProgress() {
|
||||
return 0.5;
|
||||
}
|
||||
getHost() {
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue