Skip to content
This repository has been archived by the owner on Feb 21, 2022. It is now read-only.

Commit

Permalink
Add GCP infrastructure tracking (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
bayotop authored and paveldedik committed Sep 19, 2019
1 parent f22267e commit fd3a67d
Show file tree
Hide file tree
Showing 14 changed files with 423 additions and 28 deletions.
1 change: 1 addition & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ services:
worker:
environment:
DATABASE_URL: 'postgres://postgres:postgres@postgres/postgres'
GCP_SERVICE_KEY:
ZOO_AUDITING_CHECKS: dummy_standards
ZOO_DEBUG: '1'
ZOO_DATADOG_API_KEY:
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ services:
build: .
environment:
- DATABASE_URL
- GCP_SERVICE_KEY
- SECRET_KEY
- SENTRY_DSN
- SENTRY_PUBLIC_DSN
Expand Down Expand Up @@ -42,6 +43,7 @@ services:
command: [celery, worker, -A, zoo]
environment:
- DATABASE_URL
- GCP_SERVICE_KEY
- SECRET_KEY
- SENTRY_DSN
- SENTRY_PUBLIC_DSN
Expand Down
2 changes: 2 additions & 0 deletions requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ django-extensions
django-silk
django-stronghold
dockerfile-parse
google-api-python-client
graphene
graphene-django
gunicorn[gevent]
hiredis
ipython
kubernetes
markdown
psycopg2
pygerduty
Expand Down
29 changes: 21 additions & 8 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file requirements.txt requirements.in
# pip-compile requirements.in
#
amqp==2.5.1 # via kombu
aniso8601==7.0.0 # via graphene
appnope==0.1.0 # via ipython
arrow==0.15.1
attrs==19.1.0
autopep8==1.4.4 # via django-silk
backcall==0.1.0 # via ipython
billiard==3.6.1.0 # via celery
boto3==1.9.150
botocore==1.12.150 # via boto3, s3transfer
cachetools==3.1.1 # via google-auth
celery-redbeat==0.13.0
celery==4.3.0
certifi==2019.9.11 # via requests
certifi==2019.9.11 # via kubernetes, requests
chardet==3.0.4 # via requests
colorama==0.4.1
datadog==0.30.0
Expand All @@ -33,8 +33,11 @@ django-stronghold==0.3.0
django==2.2.5
djangoql==0.13.0
dockerfile-parse==0.0.15
gevent==1.4.0 # via gunicorn
docutils==0.14 # via botocore
gevent==1.4.0 # via gunicorn
google-api-python-client==1.7.10
google-auth-httplib2==0.0.3 # via google-api-python-client
google-auth==1.6.3 # via google-api-python-client, google-auth-httplib2, kubernetes
gprof2dot==2017.9.19 # via django-silk
graphene-django==2.5.0
graphene==2.1.8
Expand All @@ -43,6 +46,7 @@ graphql-relay==2.0.0 # via graphene
greenlet==0.4.15 # via gevent
gunicorn[gevent]==19.9.0
hiredis==1.0.0
httplib2==0.13.0 # via google-api-python-client, google-auth-httplib2
idna==2.8 # via requests
importlib-metadata==0.22 # via kombu
ipython-genutils==0.2.0 # via traitlets
Expand All @@ -51,6 +55,7 @@ jedi==0.15.1 # via ipython
jinja2==2.10.1 # via django-silk
jmespath==0.9.4 # via boto3, botocore
kombu==4.6.4 # via celery
kubernetes==10.0.0
markdown==3.1.1
markupsafe==1.1.1 # via jinja2
more-itertools==7.2.0 # via zipp
Expand All @@ -64,32 +69,40 @@ promise==2.2.1 # via graphene-django, graphql-core, graphql-relay
prompt-toolkit==2.0.9 # via ipython
psycopg2==2.8.3
ptyprocess==0.6.0 # via pexpect
pyasn1-modules==0.2.5 # via google-auth
pyasn1==0.4.5 # via pyasn1-modules, rsa
pycodestyle==2.5.0 # via autopep8
pygerduty==0.38.2
pygithub==1.43.8
pygments==2.4.2 # via django-silk, ipython
pyjwt==1.7.1 # via pygithub
python-dateutil==2.8.0 # via arrow, botocore, celery-redbeat, django-silk
python-dateutil==2.8.0 # via arrow, botocore, celery-redbeat, django-silk, kubernetes
python-gitlab==1.11.0
python3-openid==3.1.0 # via django-allauth
pytz==2019.2 # via celery, django, django-silk
pyyaml==5.1.2
raven==6.10.0
redis==3.3.8
requests-oauthlib==1.2.0 # via django-allauth
requests-oauthlib==1.2.0 # via django-allauth, kubernetes
requests==2.22.0
requirements-parser==0.2.0
rsa==4.0 # via google-auth
rx==1.6.1 # via graphql-core
s3transfer==0.2.0 # via boto3
singledispatch==3.4.0.3 # via graphene-django
six==1.12.0 # via django-extensions, dockerfile-parse, graphene, graphene-django, graphql-core, graphql-relay, promise, prompt-toolkit, pygerduty, python-dateutil, python-gitlab, singledispatch, structlog, tenacity, traitlets
six==1.12.0 # via django-extensions, dockerfile-parse, google-api-python-client, google-auth, graphene, graphene-django, graphql-core, graphql-relay, kubernetes, promise, prompt-toolkit, pygerduty, python-dateutil, python-gitlab, singledispatch, structlog, tenacity, traitlets, websocket-client
sqlparse==0.3.0 # via django, django-debug-toolbar, django-silk
structlog==19.1.0
tenacity==5.1.1 # via celery-redbeat
traitlets==4.3.2 # via ipython
urllib3==1.24.3 # via botocore, requests
uritemplate==3.0.0 # via google-api-python-client
urllib3==1.24.3 # via botocore, kubernetes, requests
vine==1.3.0 # via amqp, celery
wcwidth==0.1.7 # via prompt-toolkit
websocket-client==0.56.0 # via kubernetes
whitenoise==4.1.3
wrapt==1.11.2 # via deprecated
zipp==0.6.0 # via importlib-metadata

# The following packages are considered to be unsafe in a requirements file:
# setuptools==41.2.0 # via ipython, kubernetes, markdown
2 changes: 1 addition & 1 deletion test-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# This file is autogenerated by pip-compile
# To update, run:
#
# pip-compile --output-file test-requirements.txt test-requirements.in
# pip-compile test-requirements.in
#
apipkg==1.5 # via execnet
astroid==2.2.5 # via pylint
Expand Down
89 changes: 89 additions & 0 deletions test/datacenters/test_gcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from unittest.mock import MagicMock
import pytest

from zoo.datacenters import gcp as uut, models

pytestmark = pytest.mark.django_db


def test_gcp_map_to_nodes(mocker):
mocker.patch("zoo.datacenters.utils.gcloud.GCPClient.__init__", return_value=None)
mocker.patch(
"zoo.datacenters.utils.gcloud.GCPClient.get_all_projects",
return_value=[{"projectId": "pid1"}, {"projectId": "pid2"}],
)
mocker.patch(
"zoo.datacenters.utils.gcloud.GCPClient.get_forwarding_rules",
return_value=[
{
"id": "test1",
"loadBalancingScheme": "EXTERNAL",
"IPAddress": "1.1.1.1",
"portRange": "443-443",
},
{
"id": "test2",
"loadBalancingScheme": "INTERNAL",
"IPAddress": "2.2.2.2",
"portRange": "443-443",
},
],
)
mocker.patch(
"zoo.datacenters.utils.GCPClient.get_all_clusters",
return_value=[{"name": "test", "zone": "europe-test"}],
)
mocker.patch(
"zoo.datacenters.utils.kube.KubernetesClient.__init__", return_value=None
)

workload = MagicMock()
image1 = MagicMock()
image2 = MagicMock()
image1.image = "test/image:0.0.1"
image2.image = "test/image2:0.0.2"

workload.metadata.namespace = "namespace-test"
workload.metadata.name = "resource-test"
workload.spec.template.spec.containers = [image1, image2]

mocker.patch(
"zoo.datacenters.utils.kube.KubernetesClient.iter_workloads",
return_value={"test-type": [workload]},
)

uut.map_to_nodes()

root = models.InfraNode.objects.get(kind=models.NodeKind.GCP_ROOT_PROJ)

projects = {project.value: project for project in root.targets.all()}
assert set(projects) == {"pid1", "pid2"}

ctx = "gke_pid1_europe-test_test"
clusters = {
cluster.value: cluster
for cluster in projects["pid1"].targets.filter(
kind=models.NodeKind.GCP_CLUSTER_NAME
)
}
assert set(clusters) == {ctx}

ip_rules = {
cluster.value: cluster
for cluster in projects["pid1"].targets.filter(
kind=models.NodeKind.GCP_IP_RULE_NAME
)
}
assert set(ip_rules) == {"test1:1.1.1.1:443-443"}

workloads = {
workload.value: workload
for workload in clusters["gke_pid1_europe-test_test"].targets.all()
}
full_name = "test-type:namespace-test/resource-test"
assert set(workloads) == {f"{ctx}:{full_name}"}

images = {
image.value: image for image in workloads[f"{ctx}:{full_name}"].targets.all()
}
assert set(images) == {"test/image:0.0.1", "test/image2:0.0.2"}
3 changes: 3 additions & 0 deletions zoo/base/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
RANCHER_API_URL=(str, None),
RANCHER_ACCESS_KEY=(str, None),
RANCHER_SECRET_KEY=(str, None),
GCP_SERVICE_KEY=(dict, {}),
)

SITE_ROOT = str(root)
Expand Down Expand Up @@ -247,4 +248,6 @@
RANCHER_ACCESS_KEY = env("RANCHER_ACCESS_KEY")
RANCHER_SECRET_KEY = env("RANCHER_SECRET_KEY")

GCP_SERVICE_KEY = env("GCP_SERVICE_KEY")

logs.configure_structlog(DEBUG)
91 changes: 91 additions & 0 deletions zoo/datacenters/gcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from django.db import transaction
import googleapiclient

from .models import InfraNode, NodeKind
from .utils import GCPClient, KubernetesClient

CLUSTER_IDENTIFIER = "gke_{project_id}_{zone}_{name}"


def _workload_identifier(cluster, resource_type, resource):
return f"{cluster}:{resource_type}:{resource.metadata.namespace}/{resource.metadata.name}"


def _map_workloads(workloads, cluster_ctx, cluster_node):
for resource_type, resources in workloads.items():
for resource in resources:
workload_node = InfraNode.get_or_create_node(
kind=NodeKind.GCP_WORKLOAD_NAME,
value=_workload_identifier(cluster_ctx, resource_type, resource),
source=cluster_node,
)

if resource_type == "cronjobs":
containers = resource.spec.job_template.spec.template.spec.containers
else:
containers = resource.spec.template.spec.containers

for container in containers:
InfraNode.get_or_create_node(
kind=NodeKind.DOCKER_IMAGE_UUID,
value=container.image,
source=workload_node,
)


@transaction.atomic
def map_to_nodes():
"""Map GCP projects to GCP services.
Creates records in the InfraNode table of the following kinds:
- ``gcp.root.proj`` - root node for all GCP projects
- ``gcp.proj.id`` - GCP project ID
- ``gcp.ip_rule.name`` - GCP forwarding rule name
- ``gcp.cluster.name`` - GCP cluster name
- ``gcp.workload.name`` - GCP workload name (including the namespace)
- ``docker.image.uuid`` - Docker image UUID
"""
root = InfraNode.get_or_create_node(kind=NodeKind.GCP_ROOT_PROJ, value="*")
gcloud = GCPClient()

for project in gcloud.get_all_projects():
if project["projectId"].startswith("sys-"):
# skip "shadow" projects
# see https://skypicker.slack.com/archives/C1XN8EPAP/p1568641244031000
continue

project_node = InfraNode.get_or_create_node(
kind=NodeKind.GCP_PROJ_ID, value=project["projectId"], source=root
)

try:
# skip projects without billing enabled
ip_rules = list(gcloud.get_forwarding_rules(project["projectId"]))
except googleapiclient.errors.HttpError:
continue

# currently not used anywhere
for ip_rule in ip_rules:
port_range = f":{ip_rule['portRange']}" if "portRange" in ip_rule else ""
if ip_rule["loadBalancingScheme"] == "EXTERNAL":
InfraNode.get_or_create_node(
kind=NodeKind.GCP_IP_RULE_NAME,
value=f"{ip_rule['id']}:{ip_rule['IPAddress']}{port_range}",
source=project_node,
)

for cluster in gcloud.get_all_clusters(project["projectId"]):
cluster_ctx = CLUSTER_IDENTIFIER.format(
project_id=project["projectId"],
zone=cluster["zone"],
name=cluster["name"],
)
cluster_node = InfraNode.get_or_create_node(
kind=NodeKind.GCP_CLUSTER_NAME, value=cluster_ctx, source=project_node
)

kube = KubernetesClient(cluster)
workloads = kube.iter_workloads()

_map_workloads(workloads, cluster_ctx, cluster_node)
Loading

0 comments on commit fd3a67d

Please sign in to comment.