Skip to content

Commit

Permalink
Add support for client certificates (and documentation)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomdee committed Apr 18, 2016
1 parent ea9d68b commit bc243bc
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 46 deletions.
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,21 @@ any orchestrator which makes use of the [CNI networking specification][cni].

This repository includes a top-level CNI networking plugin, as well as a CNI IPAM plugin which makes use of Calico IPAM.

The [calico-containers repository][calico-containers] contains getting started guides for a number of scenarios, as well as more detailed documentation regarding our CNI integration.
For details of configuration, see the [configuration.md][config] file.

The [calico-containers repository][calico-containers] contains getting started guides for a number of scenarios, as well as more detailed documentation regarding our CNI integration.

To learn more about CNI, visit the [appc/cni][cni] repo.

## Building the plugins and running tests
To build the Calico Networking Plugin for CNI locally, clone this repository and run `make`. This will build both CNI plugin binaries and run the unit and fv tests.
To build the Calico Networking Plugin for CNI locally, clone this repository and run `make`. This will build both CNI plugin binaries and run the unit and fv tests.

- To just build the binaries, with no tests, run `make binary`. This will produce `dist/calico` and `dist/calico-ipam`.
- To just build the binaries, with no tests, run `make binary`. This will produce `dist/calico` and `dist/calico-ipam`.
- To only run the unit tests, simply run `make ut`.
- To only run the fv tests, simply run `make fv`.

[cni]: https://github.com/appc/cni
[config]: configuration.md
[calico-containers]: https://github.com/projectcalico/calico-containers/blob/master/docs/cni/kubernetes/README.md

[![Analytics](https://calico-ga-beacon.appspot.com/UA-52125893-3/calico-cni/README.md?pixel)](https://github.com/igrigorik/ga-beacon)
12 changes: 5 additions & 7 deletions calico.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,6 @@ def __init__(self, network_config, env):
The hostname to register endpoints under.
"""

self.policy_driver = get_policy_driver(self.k8s_pod_name,
self.k8s_namespace,
self.network_config)
"""
Chooses the correct policy driver based on the given configuration
"""

self.container_engine = get_container_engine(self.k8s_pod_name)
"""
Chooses the correct container engine based on the given configuration.
Expand Down Expand Up @@ -165,6 +158,11 @@ def __init__(self, network_config, env):
# Append any existing args - if they are set.
self.ipam_env[CNI_ARGS_ENV] += ";%s" % env.get(CNI_ARGS_ENV)

self.policy_driver = get_policy_driver(self)
"""
Chooses the correct policy driver based on the given configuration
"""

def execute(self):
"""
Execute the CNI plugin - uses the given CNI_COMMAND to determine
Expand Down
3 changes: 3 additions & 0 deletions calico_cni/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
K8S_POD_NAME = "K8S_POD_NAME"
K8S_POD_NAMESPACE = "K8S_POD_NAMESPACE"
K8S_POD_INFRA_CONTAINER_ID = "K8S_POD_INFRA_CONTAINER_ID"
K8S_CLIENT_CERTIFICATE_VAR = "k8s_client_certificate"
K8S_CLIENT_KEY_VAR = "k8s_client_key"
K8S_CERTIFICATE_AUTHORITY_VAR = "k8s_certificate_authority"

# Constants for getting Calico configuration from the network
# configuration file.
Expand Down
62 changes: 47 additions & 15 deletions calico_cni/policy_drivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,17 @@ class KubernetesAnnotationDriver(DefaultPolicyDriver):
"""
Implements network policy for Kubernetes using annotations.
"""
def __init__(self, pod_name, namespace, auth_token, api_root):

def __init__(self, pod_name, namespace, auth_token, api_root,
client_certificate, client_key, certificate_authority):
self.pod_name = pod_name
self.namespace = namespace
self.policy_parser = calico_cni.policy_parser.PolicyParser(namespace)
self.auth_token = auth_token
self.api_root = api_root
self.auth_token = auth_token
self.client_certificate = client_certificate
self.client_key = client_key
self.certificate_authority = certificate_authority or False
self.api_root = api_root
self.profile_name = "%s_%s" % (namespace, pod_name)
self._annotation_key = "projectcalico.org/policy"
self.ns_tag = self._escape_chars("namespace=%s" % self.namespace)
Expand Down Expand Up @@ -256,7 +261,20 @@ def _get_api_pod(self):
# Perform the API query and handle the result.
try:
_log.debug('Querying Kubernetes API for Pod: %s', path)
response = session.get(path, verify=False)

if self.client_certificate and self.client_key:
_log.debug("Using client certificate for Query API. "
"cert: %s, key: %s",
self.client_certificate,
self.client_key)
cert = (self.client_certificate,
self.client_key)
response = session.get(path, cert=cert,
verify=self.certificate_authority)
else:
_log.debug('Using direct connection for query API')
response = session.get(path,
verify=self.certificate_authority)
except BaseException, e:
_log.exception("Exception hitting Kubernetes API")
raise ApplyProfileError("Error querying Kubernetes API",
Expand Down Expand Up @@ -347,14 +365,14 @@ def __init__(self, msg=None, details=None):
self.details = details


def get_policy_driver(k8s_pod_name, k8s_namespace, net_config):
def get_policy_driver(cni_plugin):
"""Returns a policy driver based on CNI configuration arguments.
:return: a policy driver
"""
# Extract policy config and network name.
policy_config = net_config.get(POLICY_KEY, {})
network_name = net_config["name"]
policy_config = cni_plugin.network_config.get(POLICY_KEY, {})
network_name = cni_plugin.network_config["name"]
policy_type = policy_config.get("type")
supported_policy_types = [None,
POLICY_MODE_KUBERNETES,
Expand All @@ -367,14 +385,25 @@ def get_policy_driver(k8s_pod_name, k8s_namespace, net_config):
sys.exit(ERR_CODE_GENERIC)

# Determine which policy driver to use.
if k8s_pod_name:
if cni_plugin.running_under_k8s:
# Running under Kubernetes - decide which Kubernetes driver to use.
if policy_type == POLICY_MODE_KUBERNETES_ANNOTATIONS or \
policy_type == POLICY_MODE_KUBERNETES:
assert k8s_namespace, "Missing Kubernetes namespace"
k8s_auth_token = policy_config.get(AUTH_TOKEN_KEY)
k8s_api_root = policy_config.get(API_ROOT_KEY,
assert cni_plugin.k8s_namespace, "Missing Kubernetes namespace"
auth_token = policy_config.get(AUTH_TOKEN_KEY)
api_root = policy_config.get(API_ROOT_KEY,
"https://10.100.0.1:443/api/v1/")
client_certificate = policy_config.get(K8S_CLIENT_CERTIFICATE_VAR)
client_key = policy_config.get(K8S_CLIENT_KEY_VAR)
certificate_authority = policy_config.get(
K8S_CERTIFICATE_AUTHORITY_VAR)

if (client_key and not os.path.isfile(client_key)) or \
(client_certificate and not os.path.isfile(client_certificate)) or \
(certificate_authority and not os.path.isfile(certificate_authority)):
print_cni_error(ERR_CODE_GENERIC,
"certificates provided but files don't exist")
sys.exit(ERR_CODE_GENERIC)

if policy_type == POLICY_MODE_KUBERNETES:
_log.debug("Using Kubernetes Policy Driver")
Expand All @@ -383,10 +412,13 @@ def get_policy_driver(k8s_pod_name, k8s_namespace, net_config):
_log.debug("Using Kubernetes Annotation Policy Driver")
driver_cls = KubernetesAnnotationDriver

driver_args = [k8s_pod_name,
k8s_namespace,
k8s_auth_token,
k8s_api_root]
driver_args = [cni_plugin.k8s_pod_name,
cni_plugin.k8s_namespace,
auth_token,
api_root,
client_certificate,
client_key,
certificate_authority]
else:
_log.debug("Using Kubernetes Driver - no policy")
driver_cls = KubernetesNoPolicyDriver
Expand Down
68 changes: 68 additions & 0 deletions configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# CNI Configuration

The Calico CNI plugin is configured through the standard CNI [configuration mechanism](https://github.com/appc/cni/blob/master/SPEC.md#network-configuration)

A minimal configuration file that uses Calico for networking and IPAM looks like this
```json
{
"name": "any_name",
"type": "calico",
"ipam": {
"type": "calico-ipam"
}
}
```

Additional configuration can be added as detailed below.

## Generic
### Etcd location
Specify the location of your etcd cluster using either
* `etcd_authority` (default is `127.0.0.1:2379`)
* `etcd_endpoints` (no default. Format is comma separated list of etcd servers e.g. `http://1.2.3.4:2379,http://5.6.7.8:2379`

If both are set then `etcd_endpoints` is used.

### Log levels
* Logging to `stderr` is controller through `log_level_stderr` (default is `NONE`)
* Logging to file is controlled through `log_level` (default is `INFO`).
* Files appear in /var/log/calico/cni/cni.log (and cni_ipam.log)
* Files are automatically rotated. 5 files of 1MB each are kept.

Possible log levels are
* CRITICAL
* ERROR
* WARNING
* INFO
* DEBUG
* NONE

### IPAM
When using Calico IPAM, the following flags determine what IP addresses should be assigned.
* `assign_ipv4` (default `true`)
* `assign_ipv6` (default `false`)

A specific IP address can be chosen by using [`CNI_ARGS`](https://github.com/appc/cni/blob/master/SPEC.md#parameters) and setting `IP` to the desired value.

## Kubernetes specific

When using the Calico CNI plugin with Kubernetes, an additional config block can be specified to control how network policy is configured. The required config block is `policy`. See the [Calico Kubernetes documentation](https://github.com/projectcalico/calico-containers/tree/master/docs/cni/kubernetes) for more information.

### Type
There are two supported policy types `k8s` and `k8s-annotations`
* [`k8s`](https://github.com/projectcalico/calico-containers/blob/master/docs/cni/kubernetes/NetworkPolicy.md) uses the Kubernetes NetworkPolicy API.
* [`k8s-annotations`](https://github.com/projectcalico/calico-containers/blob/master/docs/cni/kubernetes/AnnotationPolicy.md) is deprecated and uses annotations on pods to specify network policy.

### Kubernetes API access details
When using either policy type, the CNI plugin needs to be told how to access the Kubernetes API server.
* `k8s_api_root` (default `https://10.100.0.1:443/api/v1/`)

The CNI plugin may need to authenticate with the Kubernetes API server. The following methods are supported, none of which have default values.
* `k8s_auth_token`
* `k8s_client_certificate`
* `k8s_client_key`
* `k8s_certificate_authority`
* Verifying the API certificate against a CA only works if connecting to the API server using a hostname.


[![Analytics](https://calico-ga-beacon.appspot.com/UA-52125893-3/calico-cni/configuration.md?pixel)](https://github.com/igrigorik/ga-beacon)
98 changes: 77 additions & 21 deletions tests/unit/test_policy_drivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from pycalico.datastore import DatastoreClient
from pycalico.datastore_datatypes import Endpoint, Rule, Rules

from calico import CniPlugin
from calico_cni.constants import ERR_CODE_GENERIC
from calico_cni.policy_drivers import (ApplyProfileError,
get_policy_driver,
Expand Down Expand Up @@ -134,7 +135,7 @@ def setUp(self):
"policy": {}
}
self.driver = KubernetesAnnotationDriver(self.pod_name, self.namespace,
self.auth_token, self.api_root)
self.auth_token, self.api_root, None, None, None)
assert_equal(self.driver.profile_name, self.profile_name)

# Mock the DatastoreClient
Expand Down Expand Up @@ -256,6 +257,42 @@ def test_get_api_pod(self, m_json_load, m_session):
verify=False)
m_json_load.assert_called_once_with(pod1)

@patch('calico_cni.policy_drivers.requests.Session', autospec=True)
@patch('json.loads', autospec=True)
def test_get_api_pod_with_client_certs(self, m_json_load, m_session):
# Set up driver.
self.driver.pod_name = 'pod-1'
self.driver.namespace = 'a'
pod1 = '{"metadata": {"namespace": "a", "name": "pod-1"}}'

api_root = "http://kubernetesapi:8080/api/v1/"
self.driver.api_root = api_root
self.driver.client_certificate = "cert.pem"
self.driver.client_key = "key.pem"
self.driver.certificate_authority = "ca.pem"


get_obj = Mock()
get_obj.status_code = 200
get_obj.text = pod1

m_session_obj = Mock()
m_session_obj.headers = Mock()
m_session_obj.get.return_value = get_obj

m_session.return_value = m_session_obj
m_session_obj.__enter__ = Mock(return_value=m_session_obj)
m_session_obj.__exit__ = Mock(return_value=False)

# Call method under test
self.driver._get_api_pod()

# Assert correct data in calls.
m_session_obj.get.assert_called_once_with(
api_root + 'namespaces/a/pods/pod-1',
verify="ca.pem", cert=("cert.pem", "key.pem"))
m_json_load.assert_called_once_with(pod1)

@patch('calico_cni.policy_drivers.requests.Session', autospec=True)
@patch('json.loads', autospec=True)
def test_get_pod_config_error(self, m_json_load, m_session):
Expand Down Expand Up @@ -351,7 +388,7 @@ def setUp(self):
self.network_name = "net-name"
self.namespace = "default"
self.driver = KubernetesPolicyDriver(self.network_name, self.namespace,
None, None)
None, None, None, None, None)

# Mock the DatastoreClient
self.client = MagicMock(spec=DatastoreClient)
Expand Down Expand Up @@ -383,46 +420,65 @@ def test_remove_profile(self):
class GetPolicyDriverTest(unittest.TestCase):

def test_get_policy_driver_default_k8s(self):
k8s_pod_name = "podname"
k8s_namespace = "namespace"
config = {"name": "testnetwork"}
driver = get_policy_driver(k8s_pod_name, k8s_namespace, config)
cni_plugin = Mock(spec=CniPlugin)
cni_plugin.network_config = {"name": "testnetwork"}
cni_plugin.k8s_pod_name = "podname"
cni_plugin.k8s_namespace = "namespace"
cni_plugin.running_under_k8s = True
driver = get_policy_driver(cni_plugin)
assert_true(isinstance(driver, KubernetesNoPolicyDriver))

def test_get_policy_driver_k8s_annotations(self):
k8s_pod_name = "podname"
k8s_namespace = "namespace"
config = {"name": "testnetwork"}
config["policy"] = {"type": "k8s-annotations"}
driver = get_policy_driver(k8s_pod_name, k8s_namespace, config)
cni_plugin = Mock(spec=CniPlugin)
cni_plugin.network_config = {"name": "testnetwork",
"policy": {"type": "k8s-annotations"}}
cni_plugin.k8s_pod_name = "podname"
cni_plugin.k8s_namespace = "namespace"
cni_plugin.running_under_k8s = True
driver = get_policy_driver(cni_plugin)
assert_true(isinstance(driver, KubernetesAnnotationDriver))

def test_get_policy_driver_k8s(self):
k8s_pod_name = "podname"
k8s_namespace = "namespace"
config = {"name": "testnetwork"}
config["policy"] = {"type": "k8s"}
driver = get_policy_driver(k8s_pod_name, k8s_namespace, config)
cni_plugin = Mock(spec=CniPlugin)
cni_plugin.network_config = {"name": "testnetwork", "policy":{"type": "k8s"}}
cni_plugin.k8s_pod_name = "podname"
cni_plugin.k8s_namespace = "namespace"
cni_plugin.running_under_k8s = True
driver = get_policy_driver(cni_plugin)
assert_true(isinstance(driver, KubernetesPolicyDriver))

def test_get_unknown_policy_driver(self):
config = {"name": "n", "policy": {"type": "madeup"}}
cni_plugin = Mock(spec=CniPlugin)
cni_plugin.network_config = config
with assert_raises(SystemExit) as err:
get_policy_driver(cni_plugin)
e = err.exception
assert_equal(e.code, ERR_CODE_GENERIC)

def test_missing_cert(self):
config = {"name": "n", "policy": {"type": "k8s", "k8s_client_certificate":"surely this can't exist?"}}
cni_plugin = Mock(spec=CniPlugin)
cni_plugin.network_config = config
cni_plugin.running_under_k8s = True
cni_plugin.k8s_pod_name = "podname"
cni_plugin.k8s_namespace = "namespace"
with assert_raises(SystemExit) as err:
get_policy_driver(None, None, config)
get_policy_driver(cni_plugin)
e = err.exception
assert_equal(e.code, ERR_CODE_GENERIC)

@patch("calico_cni.policy_drivers.DefaultPolicyDriver", autospec=True)
def test_get_policy_driver_value_error(self, m_driver):
# Mock
m_driver.side_effect = ValueError
k8s_pod_name = None
k8s_namespace = None
config = {"name": "testnetwork"}
cni_plugin = Mock(spec=CniPlugin)
cni_plugin.network_config = {"name": "testnetwork"}
cni_plugin.running_under_k8s = False

# Call
with assert_raises(SystemExit) as err:
get_policy_driver(k8s_pod_name, k8s_namespace, config)
get_policy_driver(cni_plugin)
e = err.exception
assert_equal(e.code, ERR_CODE_GENERIC)

0 comments on commit bc243bc

Please sign in to comment.