UNPKG

@adpt/cloud

Version:
287 lines 10.8 kB
"use strict"; /* * Copyright 2018-2020 Unbounded Systems, LLC * * 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. */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); // This is to deal with the long URLs in doc comments. // tslint:disable: max-line-length const core_1 = tslib_1.__importStar(require("@adpt/core")); const utils_1 = require("@adpt/utils"); const lodash_1 = require("lodash"); const NetworkService_1 = require("../NetworkService"); const common_1 = require("./common"); const k8s_observer_1 = require("./k8s_observer"); const manifest_support_1 = require("./manifest_support"); const Resource_1 = require("./Resource"); function toServiceType(scope) { switch (scope) { case "cluster-internal": case undefined: return "ClusterIP"; case "cluster-public": return "NodePort"; case "external": return "LoadBalancer"; default: throw new Error(`Service: NetworkService scope '${scope}' not mapped to a Kubernetes service type`); } } /** * Convert {@link NetworkService} props to {@link k8s.Service} spec * @param abstractProps - props to convert * @returns Kubernetes spec corresponding to `abstractProps` * * @internal */ function k8sServiceProps(abstractProps) { if (typeof abstractProps.port !== "number") throw new Error(`Service: Port string not yet implemented`); if (abstractProps.ip != null) throw new Error(`Service: IP not yet implemented`); if (abstractProps.name != null) throw new Error(`Service: name not yet implemented`); const port = { port: abstractProps.port, targetPort: NetworkService_1.targetPort(abstractProps), }; if (abstractProps.protocol != null) port.protocol = abstractProps.protocol; const ret = { key: abstractProps.key, ports: [port], selector: abstractProps.endpoint, type: toServiceType(abstractProps.scope), }; return ret; } exports.k8sServiceProps = k8sServiceProps; const defaultProps = { sessionAffinity: "None", type: "ClusterIP", }; function findInArray(arr, keyProp, key) { if (!arr) return undefined; for (const item of arr) { if (item[keyProp] === key) return item; } return undefined; } const noExternalName = { noName: true }; function isNoExternalName(x) { return x === noExternalName; } async function getExternalName(props) { const log = console; //FIXME(manishv) Use proper logger here const resourceHand = props.handle; const resourceElem = resourceHand.target; if (!resourceElem) return noExternalName; //This should not be able to happen if (!core_1.isMountedElement(resourceElem)) return noExternalName; //Should not be possible //Don't fetch status if we don't need it if (!(props.type === "LoadBalancer" || props.type === "ExternalName")) return noExternalName; let statusTop; try { statusTop = await resourceElem.status(); } catch (e) { //Status not available yet if (e.message.startsWith("Resource not found")) return core_1.waiting("Waiting for resource to be created"); throw e; } if (!statusTop) return core_1.waiting("Waiting for status from k8s"); const spec = statusTop.spec; const status = statusTop.status; if (!status) return core_1.waiting("Waiting for status from k8s"); if (spec.type === "LoadBalancer") { if (status.loadBalancer === undefined) return core_1.waiting("Waiting for loadBlancer status from k8s"); const ingresses = status.loadBalancer.ingress; if (ingresses == null) return core_1.waiting("Waiting for Ingress IP"); if (typeof ingresses === "string") return ingresses; if (Array.isArray(ingresses)) { if (ingresses.length === 0) return noExternalName; if (ingresses.length !== 1) log.warn(`Multiple k8s LoadBalancer ingresses returned, using only one: ${ingresses}`); for (const ingress of ingresses) { if (ingress.hostname) return ingress.hostname; if (ingress.ip) return ingress.ip; } } return noExternalName; } if (spec.type === "ExternalName" && spec.externalName) return spec.externalName; return noExternalName; } /** * Native Kubernetes Service resource * * @remarks * * Implements the {@link NetworkServiceInstance} interface. * * @public */ function Service(propsIn) { const props = propsIn; const helpers = core_1.useBuildHelpers(); const deployID = helpers.deployID; if (props.ports && (props.ports.length > 1)) { for (const port of props.ports) { if (port.name === undefined) throw new Error("Service with multiple ports but no name on port"); } } const [externalName, setExternalName] = core_1.useState(undefined); const [epSelector, updateSelector] = core_1.useState(undefined); const manifest = makeSvcManifest(props, { endpointSelector: epSelector }); core_1.useImperativeMethods(() => ({ hostname: (scope) => { const resourceHand = props.handle; const resourceElem = resourceHand.target; if (!resourceElem) return undefined; if (scope && scope === NetworkService_1.NetworkScope.external) { if (isNoExternalName(externalName)) throw new Error("External name request for element, but no external name available"); if (typeof externalName === "string") return externalName; return undefined; } else { const resourceName = manifest_support_1.resourceElementToName(resourceElem, deployID); const namespace = common_1.computeNamespaceFromMetadata(manifest.metadata); return `${resourceName}.${namespace}.svc.cluster.local.`; } }, port: (name) => { if (name) { const item = findInArray(props.ports, "name", name); if (!item) return undefined; return item.port; } else if (props.ports) { //Should it be an error to ask for ports without a name when there is more than one? return props.ports[0].port; } else { return undefined; } } })); core_1.useDeployedWhen(async () => { const statusName = await getExternalName(props); if ((typeof statusName === "string") || isNoExternalName(statusName)) { setExternalName(statusName); return true; } return statusName; }); updateSelector(async () => { const { selector: ep } = props; if (!core_1.isHandle(ep)) return {}; if (!ep.target) return {}; if (!core_1.isMountedElement(ep.target)) return {}; if (ep.target.componentType !== Resource_1.Resource) { throw new Error(`Cannot handle k8s.Service endpoint of type ${ep.target.componentType.name}`); } const epProps = ep.target.props; if (epProps.kind !== "Pod") { throw new Error(`Cannot have k8s.Service endpoint of kind ${epProps.kind}`); } return utils_1.removeUndef({ adaptName: manifest_support_1.resourceElementToName(ep.target, deployID) }); }); return (core_1.default.createElement(Resource_1.Resource, { key: props.key, config: props.config, kind: manifest.kind, metadata: manifest.metadata, spec: manifest.spec })); } exports.Service = Service; // TODO: The "as any" is a workaround for an api-extractor bug. See issue #185. Service.defaultProps = defaultProps; Service.status = async (_props, _observe, buildData) => { const succ = buildData.successor; if (!succ) return undefined; return succ.status(); }; function makeSvcManifest(props, options) { const { config, key, handle } = props, spec = tslib_1.__rest(props, ["config", "key", "handle"]); // Explicit default for ports.protocol if (spec.ports) { for (const p of spec.ports) { if (p.protocol === undefined) p.protocol = "TCP"; } } if (spec.type === "LoadBalancer") { if (spec.sessionAffinity === undefined) spec.sessionAffinity = "None"; if (spec.externalTrafficPolicy === undefined) spec.externalTrafficPolicy = "Cluster"; } return { kind: "Service", metadata: {}, spec: Object.assign({}, spec, { selector: core_1.isHandle(spec.selector) ? options.endpointSelector : spec.selector }), config, }; } function deployedWhen(statusObj) { const status = statusObj; // There doesn't appear to be much actual status for a // service like there is for a Pod. if (status == null || !lodash_1.isObject(status.status)) { return core_1.waiting(`Kubernetes cluster returned invalid status for Service`); } return true; } /** @internal */ exports.serviceResourceInfo = { kind: "Service", apiName: "services", deployedWhen, statusQuery: async (props, observe, buildData) => { const obs = await observe(k8s_observer_1.K8sObserver, core_1.gql ` query ($name: String!, $kubeconfig: JSON!, $namespace: String!) { withKubeconfig(kubeconfig: $kubeconfig) { readCoreV1NamespacedService(name: $name, namespace: $namespace) @all(depth: 100) } }`, { name: manifest_support_1.resourceIdToName(props.key, buildData.id, buildData.deployID), kubeconfig: props.config.kubeconfig, namespace: common_1.computeNamespaceFromMetadata(props.metadata) }); return obs.withKubeconfig.readCoreV1NamespacedService; }, }; manifest_support_1.registerResourceKind(exports.serviceResourceInfo); //# sourceMappingURL=Service.js.map