@adpt/cloud
Version:
AdaptJS cloud component library
287 lines • 10.8 kB
JavaScript
;
/*
* 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