"use strict";
// Copyright 2016-2020, Pulumi Corporation.
//
// 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.
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const grpc = __importStar(require("@grpc/grpc-js"));
const log = __importStar(require("../log"));
const output_1 = require("../output");
const resource = __importStar(require("../resource"));
const config = __importStar(require("../runtime/config"));
const rpc = __importStar(require("../runtime/rpc"));
const settings = __importStar(require("../runtime/settings"));
const localState = __importStar(require("../runtime/state"));
const internals_1 = require("./internals");
const anyproto = __importStar(require("google-protobuf/google/protobuf/any_pb"));
const emptyproto = __importStar(require("google-protobuf/google/protobuf/empty_pb"));
const structproto = __importStar(require("google-protobuf/google/protobuf/struct_pb"));
const plugproto = __importStar(require("../proto/plugin_pb"));
const provrpc = __importStar(require("../proto/provider_grpc_pb"));
const provproto = __importStar(require("../proto/provider_pb"));
const statusproto = __importStar(require("../proto/status_pb"));
class Server {
    constructor(engineAddr, provider, uncaughtErrors) {
        this._callbacks = new Map();
        this.engineAddr = engineAddr;
        this.provider = provider;
        this.uncaughtErrors = uncaughtErrors;
        // When we catch an uncaught error, we need to respond to the inflight call/construct gRPC requests
        // with the error to avoid a hang.
        const uncaughtHandler = (err) => {
            if (!this.uncaughtErrors.has(err)) {
                this.uncaughtErrors.add(err);
            }
            // terminate the outstanding gRPC requests.
            this._callbacks.forEach((callback) => callback(err, undefined));
        };
        process.on("uncaughtException", uncaughtHandler);
        // @ts-ignore 'unhandledRejection' will almost always invoke uncaughtHandler with an Error. so
        // just suppress the TS strictness here.
        process.on("unhandledRejection", uncaughtHandler);
    }
    // Misc. methods
    cancel(call, callback) {
        callback(undefined, new emptyproto.Empty());
    }
    attach(call, callback) {
        const req = call.request;
        const host = req.getAddress();
        this.engineAddr = host;
        callback(undefined, new emptyproto.Empty());
    }
    getPluginInfo(call, callback) {
        const resp = new plugproto.PluginInfo();
        resp.setVersion(this.provider.version);
        callback(undefined, resp);
    }
    getSchema(call, callback) {
        const req = call.request;
        if (req.getVersion() !== 0) {
            callback(new Error(`unsupported schema version ${req.getVersion()}`), undefined);
        }
        const resp = new provproto.GetSchemaResponse();
        resp.setSchema(this.provider.schema || "{}");
        callback(undefined, resp);
    }
    // Config methods
    checkConfig(call, callback) {
        callback({
            code: grpc.status.UNIMPLEMENTED,
            details: "Not yet implemented: CheckConfig",
        }, undefined);
    }
    diffConfig(call, callback) {
        callback({
            code: grpc.status.UNIMPLEMENTED,
            details: "Not yet implemented: DiffConfig",
        }, undefined);
    }
    configure(call, callback) {
        const resp = new provproto.ConfigureResponse();
        resp.setAcceptsecrets(true);
        resp.setAcceptresources(true);
        resp.setAcceptoutputs(true);
        callback(undefined, resp);
    }
    // CRUD resource methods
    check(call, callback) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const req = call.request;
                const resp = new provproto.CheckResponse();
                const olds = req.getOlds().toJavaScript();
                const news = req.getNews().toJavaScript();
                let inputs = news;
                let failures = [];
                if (this.provider.check) {
                    const result = yield this.provider.check(req.getUrn(), olds, news);
                    if (result.inputs) {
                        inputs = result.inputs;
                    }
                    if (result.failures) {
                        failures = result.failures;
                    }
                }
                else {
                    // If no check method was provided, propagate the new inputs as-is.
                    inputs = news;
                }
                resp.setInputs(structproto.Struct.fromJavaScript(inputs));
                if (failures.length !== 0) {
                    const failureList = [];
                    for (const f of failures) {
                        const failure = new provproto.CheckFailure();
                        failure.setProperty(f.property);
                        failure.setReason(f.reason);
                        failureList.push(failure);
                    }
                    resp.setFailuresList(failureList);
                }
                callback(undefined, resp);
            }
            catch (e) {
                console.error(`${e}: ${e.stack}`);
                callback(e, undefined);
            }
        });
    }
    diff(call, callback) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const req = call.request;
                const resp = new provproto.DiffResponse();
                const olds = req.getOlds().toJavaScript();
                const news = req.getNews().toJavaScript();
                if (this.provider.diff) {
                    const result = yield this.provider.diff(req.getId(), req.getUrn(), olds, news);
                    if (result.changes === true) {
                        resp.setChanges(provproto.DiffResponse.DiffChanges.DIFF_SOME);
                    }
                    else if (result.changes === false) {
                        resp.setChanges(provproto.DiffResponse.DiffChanges.DIFF_NONE);
                    }
                    else {
                        resp.setChanges(provproto.DiffResponse.DiffChanges.DIFF_UNKNOWN);
                    }
                    if (result.replaces && result.replaces.length !== 0) {
                        resp.setReplacesList(result.replaces);
                    }
                    if (result.deleteBeforeReplace) {
                        resp.setDeletebeforereplace(result.deleteBeforeReplace);
                    }
                }
                callback(undefined, resp);
            }
            catch (e) {
                console.error(`${e}: ${e.stack}`);
                callback(e, undefined);
            }
        });
    }
    create(call, callback) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const req = call.request;
                if (!this.provider.create) {
                    callback(new Error(`unknown resource type ${req.getUrn()}`), undefined);
                    return;
                }
                const resp = new provproto.CreateResponse();
                const props = req.getProperties().toJavaScript();
                const result = yield this.provider.create(req.getUrn(), props);
                resp.setId(result.id);
                resp.setProperties(structproto.Struct.fromJavaScript(result.outs));
                callback(undefined, resp);
            }
            catch (e) {
                const response = grpcResponseFromError(e);
                return callback(/*err:*/ response, /*value:*/ null, /*metadata:*/ response.metadata);
            }
        });
    }
    read(call, callback) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const req = call.request;
                const resp = new provproto.ReadResponse();
                const id = req.getId();
                const props = req.getProperties().toJavaScript();
                if (this.provider.read) {
                    const result = yield this.provider.read(id, req.getUrn(), props);
                    resp.setId(result.id);
                    resp.setProperties(structproto.Struct.fromJavaScript(result.props));
                    resp.setInputs(result.inputs === undefined ? undefined : structproto.Struct.fromJavaScript(result.inputs));
                }
                else {
                    // In the event of a missing read, simply return back the input state.
                    resp.setId(id);
                    resp.setProperties(req.getProperties());
                }
                callback(undefined, resp);
            }
            catch (e) {
                console.error(`${e}: ${e.stack}`);
                callback(e, undefined);
            }
        });
    }
    update(call, callback) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const req = call.request;
                const resp = new provproto.UpdateResponse();
                const olds = req.getOlds().toJavaScript();
                const news = req.getNews().toJavaScript();
                let result = {};
                if (this.provider.update) {
                    result = (yield this.provider.update(req.getId(), req.getUrn(), olds, news)) || {};
                }
                resp.setProperties(structproto.Struct.fromJavaScript(result.outs));
                callback(undefined, resp);
            }
            catch (e) {
                const response = grpcResponseFromError(e);
                return callback(/*err:*/ response, /*value:*/ null, /*metadata:*/ response.metadata);
            }
        });
    }
    delete(call, callback) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const req = call.request;
                const props = req.getProperties().toJavaScript();
                if (this.provider.delete) {
                    yield this.provider.delete(req.getId(), req.getUrn(), props);
                }
                callback(undefined, new emptyproto.Empty());
            }
            catch (e) {
                console.error(`${e}: ${e.stack}`);
                callback(e, undefined);
            }
        });
    }
    construct(call, callback) {
        return __awaiter(this, void 0, void 0, function* () {
            // Setup a new async state store for this run
            const store = new localState.LocalStore();
            return localState.asyncLocalStorage.run(store, () => __awaiter(this, void 0, void 0, function* () {
                const callbackId = Symbol("id");
                this._callbacks.set(callbackId, callback);
                try {
                    const req = call.request;
                    const type = req.getType();
                    const name = req.getName();
                    if (!this.provider.construct) {
                        callback(new Error(`unknown resource type ${type}`), undefined);
                        return;
                    }
                    yield configureRuntime(req, this.engineAddr);
                    const inputs = yield deserializeInputs(req.getInputs(), req.getInputdependenciesMap());
                    // Rebuild the resource options.
                    const dependsOn = [];
                    for (const urn of req.getDependenciesList()) {
                        dependsOn.push(new resource.DependencyResource(urn));
                    }
                    const providers = {};
                    const rpcProviders = req.getProvidersMap();
                    if (rpcProviders) {
                        for (const [pkg, ref] of rpcProviders.entries()) {
                            providers[pkg] = createProviderResource(ref);
                        }
                    }
                    const opts = {
                        aliases: req.getAliasesList(),
                        dependsOn: dependsOn,
                        protect: req.getProtect(),
                        providers: providers,
                        parent: req.getParent() ? new resource.DependencyResource(req.getParent()) : undefined,
                    };
                    const result = yield this.provider.construct(name, type, inputs, opts);
                    const resp = new provproto.ConstructResponse();
                    resp.setUrn(yield output_1.output(result.urn).promise());
                    const [state, stateDependencies] = yield rpc.serializeResourceProperties(`construct(${type}, ${name})`, result.state);
                    const stateDependenciesMap = resp.getStatedependenciesMap();
                    for (const [key, resources] of stateDependencies) {
                        const deps = new provproto.ConstructResponse.PropertyDependencies();
                        deps.setUrnsList(yield Promise.all(Array.from(resources).map((r) => r.urn.promise())));
                        stateDependenciesMap.set(key, deps);
                    }
                    resp.setState(structproto.Struct.fromJavaScript(state));
                    // Wait for RPC operations to complete.
                    yield settings.waitForRPCs();
                    callback(undefined, resp);
                }
                catch (e) {
                    console.error(`${e}: ${e.stack}`);
                    callback(e, undefined);
                }
                finally {
                    // remove the gRPC callback context from the map of in-flight callbacks
                    this._callbacks.delete(callbackId);
                }
            }));
        });
    }
    call(call, callback) {
        return __awaiter(this, void 0, void 0, function* () {
            // Setup a new async state store for this run
            const store = new localState.LocalStore();
            return localState.asyncLocalStorage.run(store, () => __awaiter(this, void 0, void 0, function* () {
                const callbackId = Symbol("id");
                this._callbacks.set(callbackId, callback);
                try {
                    const req = call.request;
                    if (!this.provider.call) {
                        callback(new Error(`unknown function ${req.getTok()}`), undefined);
                        return;
                    }
                    yield configureRuntime(req, this.engineAddr);
                    const args = yield deserializeInputs(req.getArgs(), req.getArgdependenciesMap());
                    const result = yield this.provider.call(req.getTok(), args);
                    const resp = new provproto.CallResponse();
                    if (result.outputs) {
                        const [ret, retDependencies] = yield rpc.serializeResourceProperties(`call(${req.getTok()})`, result.outputs);
                        const returnDependenciesMap = resp.getReturndependenciesMap();
                        for (const [key, resources] of retDependencies) {
                            const deps = new provproto.CallResponse.ReturnDependencies();
                            deps.setUrnsList(yield Promise.all(Array.from(resources).map((r) => r.urn.promise())));
                            returnDependenciesMap.set(key, deps);
                        }
                        resp.setReturn(structproto.Struct.fromJavaScript(ret));
                    }
                    if ((result.failures || []).length !== 0) {
                        const failureList = [];
                        for (const f of result.failures) {
                            const failure = new provproto.CheckFailure();
                            failure.setProperty(f.property);
                            failure.setReason(f.reason);
                            failureList.push(failure);
                        }
                        resp.setFailuresList(failureList);
                    }
                    // Wait for RPC operations to complete.
                    yield settings.waitForRPCs();
                    callback(undefined, resp);
                }
                catch (e) {
                    console.error(`${e}: ${e.stack}`);
                    callback(e, undefined);
                }
                finally {
                    // remove the gRPC callback context from the map of in-flight callbacks
                    this._callbacks.delete(callbackId);
                }
            }));
        });
    }
    invoke(call, callback) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const req = call.request;
                if (!this.provider.invoke) {
                    callback(new Error(`unknown function ${req.getTok()}`), undefined);
                    return;
                }
                const args = req.getArgs().toJavaScript();
                const result = yield this.provider.invoke(req.getTok(), args);
                const resp = new provproto.InvokeResponse();
                resp.setReturn(structproto.Struct.fromJavaScript(result.outputs));
                if ((result.failures || []).length !== 0) {
                    const failureList = [];
                    for (const f of result.failures) {
                        const failure = new provproto.CheckFailure();
                        failure.setProperty(f.property);
                        failure.setReason(f.reason);
                        failureList.push(failure);
                    }
                    resp.setFailuresList(failureList);
                }
                callback(undefined, resp);
            }
            catch (e) {
                console.error(`${e}: ${e.stack}`);
                callback(e, undefined);
            }
        });
    }
    streamInvoke(call, callback) {
        return __awaiter(this, void 0, void 0, function* () {
            callback({
                code: grpc.status.UNIMPLEMENTED,
                details: "Not yet implemented: StreamInvoke",
            }, undefined);
        });
    }
}
function configureRuntime(req, engineAddr) {
    return __awaiter(this, void 0, void 0, function* () {
        // NOTE: these are globals! We should ensure that all settings are identical between calls, and eventually
        // refactor so we can avoid the global state.
        if (engineAddr === undefined) {
            throw new Error("fatal: Missing <engine> address");
        }
        settings.resetOptions(req.getProject(), req.getStack(), req.getParallel(), engineAddr, req.getMonitorendpoint(), req.getDryrun(), req.getOrganization());
        // resetOptions doesn't reset the saved features
        yield settings.awaitFeatureSupport();
        const pulumiConfig = {};
        const rpcConfig = req.getConfigMap();
        if (rpcConfig) {
            for (const [k, v] of rpcConfig.entries()) {
                pulumiConfig[k] = v;
            }
        }
        config.setAllConfig(pulumiConfig, req.getConfigsecretkeysList());
    });
}
/**
 * deserializeInputs deserializes the inputs struct and applies appropriate dependencies.
 * @internal
 */
function deserializeInputs(inputsStruct, inputDependencies) {
    var _a, _b;
    return __awaiter(this, void 0, void 0, function* () {
        const result = {};
        const deserializedInputs = rpc.deserializeProperties(inputsStruct);
        for (const k of Object.keys(deserializedInputs)) {
            const input = deserializedInputs[k];
            const isSecret = rpc.isRpcSecret(input);
            const depsUrns = (_b = (_a = inputDependencies.get(k)) === null || _a === void 0 ? void 0 : _a.getUrnsList()) !== null && _b !== void 0 ? _b : [];
            if (!isSecret &&
                (depsUrns.length === 0 || containsOutputs(input) || (yield isResourceReference(input, depsUrns)))) {
                // If the input isn't a secret and either doesn't have any dependencies, already contains Outputs (from
                // deserialized output values), or is a resource reference, then we can return it directly without
                // wrapping it as an output.
                result[k] = input;
            }
            else {
                // Otherwise, wrap it in an output so we can handle secrets and/or track dependencies.
                // Note: If the value is or contains an unknown value, the Output will mark its value as
                // unknown automatically, so we just pass true for isKnown here.
                const deps = depsUrns.map((depUrn) => new resource.DependencyResource(depUrn));
                result[k] = new output_1.Output(deps, Promise.resolve(rpc.unwrapRpcSecret(input)), Promise.resolve(true), Promise.resolve(isSecret), Promise.resolve([]));
            }
        }
        return result;
    });
}
exports.deserializeInputs = deserializeInputs;
/**
 * Returns true if the input is a resource reference.
 */
function isResourceReference(input, deps) {
    return __awaiter(this, void 0, void 0, function* () {
        return resource.Resource.isInstance(input) && deps.length === 1 && deps[0] === (yield input.urn.promise());
    });
}
/**
 * Returns true if the deserialized input contains Outputs (deeply), excluding properties of Resources.
 * @internal
 */
function containsOutputs(input) {
    if (Array.isArray(input)) {
        for (const e of input) {
            if (containsOutputs(e)) {
                return true;
            }
        }
    }
    else if (typeof input === "object") {
        if (output_1.Output.isInstance(input)) {
            return true;
        }
        else if (resource.Resource.isInstance(input)) {
            // Do not drill into instances of Resource because they will have properties that are
            // instances of Output (e.g. urn, id, etc.) and we're only looking for instances of
            // Output that aren't associated with a Resource.
            return false;
        }
        for (const k of Object.keys(input)) {
            if (containsOutputs(input[k])) {
                return true;
            }
        }
    }
    return false;
}
exports.containsOutputs = containsOutputs;
// grpcResponseFromError creates a gRPC response representing an error from a dynamic provider's
// resource. This is typically either a creation error, in which the API server has (virtually)
// rejected the resource, or an initialization error, where the API server has accepted the
// resource, but it failed to initialize (e.g., the app code is continually crashing and the
// resource has failed to become alive).
function grpcResponseFromError(e) {
    // Create response object.
    const resp = new statusproto.Status();
    resp.setCode(grpc.status.UNKNOWN);
    resp.setMessage(e.message);
    const metadata = new grpc.Metadata();
    if (e.id) {
        // Object created successfully, but failed to initialize. Pack initialization failure into
        // details.
        const detail = new provproto.ErrorResourceInitFailed();
        detail.setId(e.id);
        detail.setProperties(structproto.Struct.fromJavaScript(e.properties || {}));
        detail.setReasonsList(e.reasons || []);
        const details = new anyproto.Any();
        details.pack(detail.serializeBinary(), "pulumirpc.ErrorResourceInitFailed");
        // Add details to metadata.
        resp.addDetails(details);
        // NOTE: `grpc-status-details-bin` is a magic field that allows us to send structured
        // protobuf data as an error back through gRPC. This notion of details is a first-class in
        // the Go gRPC implementation, and the nodejs implementation has not quite caught up to it,
        // which is why it's cumbersome here.
        metadata.add("grpc-status-details-bin", Buffer.from(resp.serializeBinary()));
    }
    return {
        code: grpc.status.UNKNOWN,
        message: e.message,
        metadata: metadata,
    };
}
function main(provider, args) {
    return __awaiter(this, void 0, void 0, function* () {
        // We track all uncaught errors here.  If we have any, we will make sure we always have a non-0 exit
        // code.
        const uncaughtErrors = new Set();
        const uncaughtHandler = (err) => {
            if (!uncaughtErrors.has(err)) {
                uncaughtErrors.add(err);
                // Use `pulumi.log.error` here to tell the engine there was a fatal error, which should
                // stop processing subsequent resource operations.
                log.error(err.stack || err.message || "" + err);
            }
        };
        process.on("uncaughtException", uncaughtHandler);
        // @ts-ignore 'unhandledRejection' will almost always invoke uncaughtHandler with an Error. so just
        // suppress the TS strictness here.
        process.on("unhandledRejection", uncaughtHandler);
        process.on("exit", (code) => {
            // If there were any uncaught errors at all, we always want to exit with an error code.
            if (code === 0 && uncaughtErrors.size > 0) {
                process.exitCode = 1;
            }
        });
        const parsedArgs = internals_1.parseArgs(args);
        // Finally connect up the gRPC client/server and listen for incoming requests.
        const server = new grpc.Server({
            "grpc.max_receive_message_length": settings.maxRPCMessageSize,
        });
        // The program receives a single optional argument: the address of the RPC endpoint for the engine.  It
        // optionally also takes a second argument, a reference back to the engine, but this may be missing.
        const engineAddr = parsedArgs === null || parsedArgs === void 0 ? void 0 : parsedArgs.engineAddress;
        server.addService(provrpc.ResourceProviderService, new Server(engineAddr, provider, uncaughtErrors));
        const port = yield new Promise((resolve, reject) => {
            server.bindAsync(`127.0.0.1:0`, grpc.ServerCredentials.createInsecure(), (err, p) => {
                if (err) {
                    reject(err);
                }
                else {
                    resolve(p);
                }
            });
        });
        // Emit the address so the monitor can read it to connect.  The gRPC server will keep the message loop alive.
        // We explicitly convert the number to a string so that Node doesn't colorize the output.
        console.log(port.toString());
    });
}
exports.main = main;
/**
 * Rehydrate the provider reference into a registered ProviderResource,
 * otherwise return an instance of DependencyProviderResource.
 */
function createProviderResource(ref) {
    const [urn] = resource.parseResourceReference(ref);
    const urnParts = urn.split("::");
    const qualifiedType = urnParts[2];
    const urnName = urnParts[3];
    const type = qualifiedType.split("$").pop();
    const typeParts = type.split(":");
    const typName = typeParts.length > 2 ? typeParts[2] : "";
    const resourcePackage = rpc.getResourcePackage(typName, /*version:*/ "");
    if (resourcePackage) {
        return resourcePackage.constructProvider(urnName, type, urn);
    }
    return new resource.DependencyProviderResource(ref);
}
//# sourceMappingURL=server.js.map