diff --git a/Makefile b/Makefile index f6fa067..a6e3b03 100644 --- a/Makefile +++ b/Makefile @@ -27,4 +27,6 @@ push: docker docker push "$(IMAGE):$(TAG)" mock: - docker run $(TTYFLAGS) -p 8080:8080 "$(IMAGE):$(TAG)" --mock + docker run $(TTYFLAGS) -p 8080:8080 "$(IMAGE):$(TAG)" --mock \ + --node-link-url-template "https://kube-web-view.example.org/clusters/{cluster}/nodes/{name}" \ + --pod-link-url-template "https://kube-web-view.example.org/clusters/{cluster}/namespaces/{namespace}/pods/{name}" diff --git a/README.rst b/README.rst index 4468013..2787467 100644 --- a/README.rst +++ b/README.rst @@ -14,8 +14,6 @@ Kubernetes Operational View :target: https://hub.docker.com/r/hjacobs/kube-ops-view :alt: Docker pulls -**This project is in a very early state, but it might already be useful.** - .. image:: screenshot.png :alt: Screenshot @@ -157,6 +155,10 @@ The following environment variables are supported: Optional Redis server to use for pub/sub events and job locking when running more than one replica. Example: ``redis://my-redis:6379`` ``SERVER_PORT`` HTTP port to listen on. It defaults to ``8080``. +``NODE_LINK_URL_TEMPLATE`` + Template to make Nodes clickable, e.g. can point to `kube-web-view `_. ``{cluster}`` (cluster ID) and ``{name}`` (Node name) will be replaced in the URL template. +``POD_LINK_URL_TEMPLATE`` + Template to make Pods clickable, e.g. can point to `kube-web-view `_. ``{cluster}`` (cluster ID), ``{namespace}`` (Pod's namespace), and ``{name}`` (Pod name) will be replaced in the URL template. Supported Browsers diff --git a/app/src/app.js b/app/src/app.js index 1e61815..092612e 100644 --- a/app/src/app.js +++ b/app/src/app.js @@ -14,13 +14,17 @@ const addWheelListener = require('./vendor/addWheelListener') export default class App { - constructor() { + constructor(config) { const params = this.parseLocationHash() + this.config = Config.fromParams(params) + this.config.nodeLinkUrlTemplate = config['node_link_url_template'] + this.config.podLinkUrlTemplate = config['pod_link_url_template'] + this.filterString = (params.get('q') && decodeURIComponent(params.get('q'))) || '' this.selectedClusters = new Set((params.get('clusters') || '').split(',').filter(x => x)) this.seenPods = new Set() - + // check localStorage, use the first function as a default option const indexSorterFn = ALL_SORTS.findIndex(obj => obj.text === (localStorage.getItem('sorterFn') || ALL_SORTS[0].text)) this.sorterFn = ALL_SORTS[indexSorterFn].value diff --git a/app/src/config.js b/app/src/config.js index d45c849..f45563f 100644 --- a/app/src/config.js +++ b/app/src/config.js @@ -13,6 +13,9 @@ export default class Config { this.maxConnectionLifetimeSeconds = 300 // consider cluster data older than 1 minute outdated this.maxDataAgeSeconds = 60 + + this.nodeLinkUrlTemplate = null + this.podLinkUrlTemplate = null } static fromParams(params) { diff --git a/app/src/node.js b/app/src/node.js index d6976f0..7fae4f3 100644 --- a/app/src/node.js +++ b/app/src/node.js @@ -70,6 +70,7 @@ export class Node extends PIXI.Graphics { topHandle.beginFill(App.current.theme.primaryColor, 1) topHandle.drawRect(0, 0, this.widthOfNodePx, App.current.heightOfTopHandlePx) topHandle.endFill() + // there is about 2.83 letters per pod const roomForText = Math.floor(2.83 * this.podsPerRow) const ellipsizedNodeName = this.node.name.length > roomForText ? this.node.name.substring(0, roomForText).concat('…') : this.node.name @@ -97,6 +98,12 @@ export class Node extends PIXI.Graphics { topHandle.on('mouseout', function () { nodeBox.tooltip.visible = false }) + if (App.current.config.nodeLinkUrlTemplate !== null) { + topHandle.buttonMode = true + topHandle.on('click', function() { + location.href = App.current.config.nodeLinkUrlTemplate.replace('{cluster}', nodeBox.cluster.cluster.id).replace('{name}', nodeBox.node.name) + }) + } const resources = this.getResourceUsage() const bars = new Bars(nodeBox, resources, nodeBox.tooltip) bars.x = 0 diff --git a/app/src/pod.js b/app/src/pod.js index 7670a0c..c629175 100644 --- a/app/src/pod.js +++ b/app/src/pod.js @@ -228,6 +228,12 @@ export class Pod extends PIXI.Graphics { podBox.filters = podBox.filters.filter(x => x != BRIGHTNESS_FILTER) this.tooltip.visible = false }) + if (App.current.config.podLinkUrlTemplate !== null) { + podBox.buttonMode = true + podBox.on('click', function() { + location.href = App.current.config.podLinkUrlTemplate.replace('{cluster}', this.cluster.cluster.id).replace('{namespace}', this.pod.namespace).replace('{name}', this.pod.name) + }) + } podBox.lineStyle(1, App.current.theme.primaryColor, 1) const w = 10 / this.pod.containers.length for (let i = 0; i < this.pod.containers.length; i++) { diff --git a/kube_ops_view/main.py b/kube_ops_view/main.py index b73efae..78dbde8 100644 --- a/kube_ops_view/main.py +++ b/kube_ops_view/main.py @@ -78,7 +78,7 @@ def index(): else: logger.error('Could not find JavaScript application bundle app*.js in {}'.format(static_build_path)) flask.abort(503, 'JavaScript application bundle not found (missing build)') - return flask.render_template('index.html', app_js=app_js, version=kube_ops_view.__version__) + return flask.render_template('index.html', app_js=app_js, version=kube_ops_view.__version__, app_config_json=json.dumps(app.app_config)) def event(cluster_ids: set): @@ -188,7 +188,10 @@ def convert(self, value, param, ctx): @click.option('--kubeconfig-contexts', type=CommaSeparatedValues(), help='List of kubeconfig contexts to use (default: use all defined contexts)', envvar='KUBECONFIG_CONTEXTS') @click.option('--query-interval', type=float, help='Interval in seconds for querying clusters (default: 5)', envvar='QUERY_INTERVAL', default=5) -def main(port, debug, mock, secret_key, redis_url, clusters: list, cluster_registry_url, kubeconfig_path, kubeconfig_contexts: list, query_interval): +@click.option('--node-link-url-template', help='Template for target URL when clicking on a Node', envvar='NODE_LINK_URL_TEMPLATE') +@click.option('--pod-link-url-template', help='Template for target URL when clicking on a Pod', envvar='POD_LINK_URL_TEMPLATE') +def main(port, debug, mock, secret_key, redis_url, clusters: list, cluster_registry_url, kubeconfig_path, kubeconfig_contexts: list, query_interval, + node_link_url_template: str, pod_link_url_template: str): logging.basicConfig(level=logging.DEBUG if debug else logging.INFO) store = RedisStore(redis_url) if redis_url else MemoryStore() @@ -196,6 +199,7 @@ def main(port, debug, mock, secret_key, redis_url, clusters: list, cluster_regis app.debug = debug app.secret_key = secret_key app.store = store + app.app_config = {'node_link_url_template': node_link_url_template, 'pod_link_url_template': pod_link_url_template} if mock: cluster_query = query_mock_cluster diff --git a/kube_ops_view/templates/index.html b/kube_ops_view/templates/index.html index fd24f68..bf1d503 100644 --- a/kube_ops_view/templates/index.html +++ b/kube_ops_view/templates/index.html @@ -23,6 +23,6 @@
Loading..
- +