Rename states and listener pattern

This commit is contained in:
Ben Schwartz 2021-07-20 15:54:31 -04:00
parent b4e5981aa5
commit fbc62f43b6
7 changed files with 63 additions and 66 deletions

View file

@ -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"

View file

@ -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+.

View file

@ -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.

View file

@ -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) {

View file

@ -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 {

View file

@ -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());
}
}
}

View file

@ -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 {