Skip to content
This repository has been archived by the owner on Oct 3, 2020. It is now read-only.

Commit

Permalink
provide Docker image and example manifest (#22)
Browse files Browse the repository at this point in the history
* provide Docker image and example manifest

* v0.1

* test Docker build on Travis

* add securityContext
  • Loading branch information
hjacobs authored Jul 22, 2018
1 parent 1828a88 commit c8d1a73
Show file tree
Hide file tree
Showing 10 changed files with 229 additions and 21 deletions.
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
sudo: required
language: python
python:
- "3.6"
services:
- docker
install:
- pip install pipenv
- pipenv install --dev
script:
- flake8
- py.test --doctest-modules
- make docker
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ RUN pipenv install --system --deploy --ignore-pipfile
COPY *.py /
COPY templates /templates

ARG VERSION=dev
RUN sed -i "s/__version__ = .*/__version__ = '${VERSION}'/" /report.py

ENTRYPOINT ["/report.py"]
17 changes: 17 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.PHONY: test docker push

IMAGE ?= hjacobs/kube-resource-report
VERSION ?= $(shell git describe --tags --always --dirty)
TAG ?= $(VERSION)

default: docker

test:
pytest

docker:
docker build --build-arg "VERSION=$(VERSION)" -t "$(IMAGE):$(TAG)" .
@echo 'Docker image $(IMAGE):$(TAG) can now be used.'

push: docker
docker push "$(IMAGE):$(TAG)"
22 changes: 20 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,32 @@ The output will be HTML files plus multiple tab-separated files:
``output/pods.tsv``
List of all pods and their CPU/memory requests and usages.


---------------------
Deploying to Minikube
---------------------

This will deploy a single pod with kube-resource-report and nginx (to serve the static HTML):

.. code-block::
$ minikube start
$ kubectl apply -f deploy/
$ pod=$(kubectl get pod -l application=kube-resource-report -o jsonpath='{.items[].metadata.name}')
$ kubectl port-forward $pod 8080:80
Now open http://localhost:8080/ in your browser.


---------------------------
Running as Docker container
---------------------------

.. code-block::
$ docker build -t kube-resource-report .
$ docker run -it --net=host -v ~/.kube:/kube -v $(pwd)/output:/out kube-resource-report --kubeconfig-path=/kube/config /out
$ kubectl proxy & # start proxy to your cluster (e.g. Minikube)
$ # run kube-resource-report and generate static HTML to ./output (this trick does not work with Docker for Mac!)
$ docker run -it --user=$(id -u) --net=host -v $(pwd)/output:/output hjacobs/kube-resource-report:0.1 /output
--------------------
Application Registry
Expand Down
3 changes: 2 additions & 1 deletion cluster_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ def __init__(self, api_server_urls: list):
generate_cluster_id(DEFAULT_CLUSTERS), DEFAULT_CLUSTERS
)
else:
config = kubernetes.client.configuration
# "load_incluster_config" set defaults in the config class
config = kubernetes.client.configuration.Configuration()
cluster = Cluster(
generate_cluster_id(config.host),
config.host,
Expand Down
66 changes: 66 additions & 0 deletions deploy/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
application: kube-resource-report
version: v0.1
name: kube-resource-report
spec:
replicas: 1
selector:
matchLabels:
application: kube-resource-report
template:
metadata:
labels:
application: kube-resource-report
version: v0.1
spec:
serviceAccount: kube-resource-report
containers:
- name: kube-resource-report
# see https://github.com/hjacobs/kube-resource-report/releases
image: hjacobs/kube-resource-report:0.1
args:
- --update-interval-minutes=1
# this is just an example, e.g. for Minikube: assume 30 USD/month cluster costs
- --additional-cost-per-cluster=30.0
- /output
volumeMounts:
- mountPath: /output
name: report-data
resources:
limits:
memory: 100Mi
requests:
cpu: 5m
memory: 50Mi
securityContext:
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
- name: nginx
image: nginx:alpine
volumeMounts:
- mountPath: /usr/share/nginx/html
name: report-data
readOnly: true
ports:
- containerPort: 80
readinessProbe:
httpGet:
path: /
port: 80
resources:
limits:
memory: 50Mi
requests:
cpu: 5m
memory: 20Mi
volumes:
- name: report-data
emptyDir: {}
sizeLimit: 500Mi



41 changes: 41 additions & 0 deletions deploy/rbac.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: kube-resource-report
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: kube-resource-report
rules:
- apiGroups: [""]
resources: ["nodes", "pods"]
verbs:
- list
- apiGroups: ["extensions"]
resources: ["ingresses"]
verbs:
- list
- apiGroups: ["metrics"]
resources: ["nodes", "pods"]
verbs:
- get
- apiGroups: [""]
resources: ["services/proxy"]
resourceNames: ["heapster"]
verbs:
- get
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: kube-resource-report
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kube-resource-report
subjects:
- kind: ServiceAccount
name: kube-resource-report
namespace: default
14 changes: 14 additions & 0 deletions deploy/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
labels:
application: kube-resource-report
name: kube-resource-report
spec:
selector:
application: kube-resource-report
type: ClusterIP
ports:
- port: 80
protocol: TCP
targetPort: 80
79 changes: 61 additions & 18 deletions report.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import requests
import cluster_discovery
import shutil
import time
from urllib.parse import urljoin
from pathlib import Path

Expand All @@ -21,7 +22,7 @@
from jinja2 import Environment, FileSystemLoader, select_autoescape
import filters

VERSION = "v0.1"
__version__ = "v0.1"

# TODO: this should be configurable
NODE_LABEL_SPOT = "aws.amazon.com/spot"
Expand Down Expand Up @@ -164,8 +165,15 @@ def query_cluster(

try:
# https://github.com/kubernetes/community/blob/master/contributors/design-proposals/instrumentation/resource-metrics-api.md
response = request(cluster, "/apis/metrics.k8s.io/v1beta1/nodes")
response.raise_for_status()
for i, url in enumerate(["/apis/metrics.k8s.io/v1beta1/nodes", '/api/v1/namespaces/kube-system/services/heapster/proxy/apis/metrics/v1alpha1/nodes']):
try:
response = request(cluster, url)
response.raise_for_status()
except Exception as e:
if i == 0:
logger.warning('Failed to query metrics: %s', e)
else:
raise
for item in response.json()["items"]:
key = item["metadata"]["name"]
node = nodes.get(key)
Expand Down Expand Up @@ -244,8 +252,15 @@ def query_cluster(

try:
# https://github.com/kubernetes/community/blob/master/contributors/design-proposals/instrumentation/resource-metrics-api.md
response = request(cluster, "/apis/metrics.k8s.io/v1beta1/pods")
response.raise_for_status()
for i, url in enumerate(["/apis/metrics.k8s.io/v1beta1/pods", '/api/v1/namespaces/kube-system/services/heapster/proxy/apis/metrics/v1alpha1/pods']):
try:
response = request(cluster, url)
response.raise_for_status()
except Exception as e:
if i == 0:
logger.warning('Failed to query metrics: %s', e)
else:
raise
for item in response.json()["items"]:
key = (item["metadata"]["namespace"], item["metadata"]["name"])
pod = pods.get(key)
Expand Down Expand Up @@ -298,7 +313,20 @@ def query_cluster(
return cluster_summary


class CommaSeparatedValues(click.ParamType):
name = 'comma_separated_values'

def convert(self, value, param, ctx):
if isinstance(value, str):
values = filter(None, value.split(','))
else:
values = value
return values


@click.command()
@click.option('--clusters', type=CommaSeparatedValues(),
help='Comma separated list of Kubernetes API server URLs (default: {})'.format(cluster_discovery.DEFAULT_CLUSTERS), envvar='CLUSTERS')
@click.option(
"--cluster-registry",
metavar="URL",
Expand Down Expand Up @@ -336,8 +364,10 @@ def query_cluster(
help="Additional fixed costs per cluster (e.g. etcd nodes, ELBs, ..)",
default=0,
)
@click.option('--update-interval-minutes', type=float, help='Update the report every X minutes (default: run once and exit)', default=0)
@click.argument("output_dir", type=click.Path(exists=True))
def main(
clusters,
cluster_registry,
kubeconfig_path,
application_registry,
Expand All @@ -348,6 +378,7 @@ def main(
include_clusters,
exclude_clusters,
additional_cost_per_cluster,
update_interval_minutes
):
"""Kubernetes Resource Report
Expand All @@ -359,21 +390,28 @@ def main(
else:
kubeconfig_path = Path(os.path.expanduser("~/.kube/config"))

generate_report(
cluster_registry,
kubeconfig_path,
application_registry,
use_cache,
no_ingress_status,
output_dir,
set(system_namespaces.split(",")),
include_clusters,
exclude_clusters,
additional_cost_per_cluster,
)
while True:
generate_report(
clusters,
cluster_registry,
kubeconfig_path,
application_registry,
use_cache,
no_ingress_status,
output_dir,
set(system_namespaces.split(",")),
include_clusters,
exclude_clusters,
additional_cost_per_cluster,
)
if update_interval_minutes > 0:
time.sleep(update_interval_minutes * 60)
else:
break


def get_cluster_summaries(
clusters: list,
cluster_registry: str,
kubeconfig_path: Path,
include_clusters: str,
Expand All @@ -387,6 +425,9 @@ def get_cluster_summaries(

if cluster_registry:
discoverer = cluster_discovery.ClusterRegistryDiscoverer(cluster_registry)
elif clusters or not kubeconfig_path.exists():
api_server_urls = clusters or []
discoverer = cluster_discovery.StaticClusterDiscoverer(api_server_urls)
else:
discoverer = cluster_discovery.KubeconfigDiscoverer(kubeconfig_path, set())

Expand Down Expand Up @@ -478,6 +519,7 @@ def resolve_application_ids(applications: dict, teams: dict, application_registr


def generate_report(
clusters,
cluster_registry,
kubeconfig_path,
application_registry,
Expand All @@ -504,6 +546,7 @@ def generate_report(

else:
cluster_summaries = get_cluster_summaries(
clusters,
cluster_registry,
kubeconfig_path,
include_clusters,
Expand Down Expand Up @@ -706,7 +749,7 @@ def generate_report(
},
"total_slack_cost": sum([a["slack_cost"] for a in applications.values()]),
"now": datetime.datetime.utcnow(),
"version": VERSION,
"version": __version__,
}
for page in ["index", "clusters", "ingresses", "teams", "applications", "pods"]:
file_name = "{}.html".format(page)
Expand Down
1 change: 1 addition & 0 deletions test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def test_generate_report(tmpdir, monkeypatch):
lambda cluster, path: MagicMock(json=lambda: responses.get(path)),
)
cluster_summaries = generate_report(
[],
"https://cluster-registry",
None,
None,
Expand Down

0 comments on commit c8d1a73

Please sign in to comment.