Skip to content

Commit ccaf090

Browse files
committed
Init Anchor, a Layer-2 CNI plugin based MacVLAN
1 parent 111e55e commit ccaf090

35 files changed

+3237
-1
lines changed

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
The MIT License
22

3-
Copyright (c) 2017-2018 Haines Chan. http://hainesc.github.io
3+
Copyright (c) 2018 Haines Chan <[email protected]>
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

Makefile

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
PACKAGE = anchor
2+
DATE ?= $(shell date +%FT%T%z)
3+
VERSION ?= $(shell git describe --tags --always --dirty --match=v* 2> /dev/null || \
4+
cat $(CURDIR)/.version 2> /dev/null || echo v0)
5+
PKGS = $(or $(PKG),$(shell env GO111MODULE=on $(GO) list ./...))
6+
TESTPKGS = $(shell env GO111MODULE=on $(GO) list -f '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' $(PKGS))
7+
BIN = $(CURDIR)/bin
8+
BUILD = $(CURDIR)/build
9+
GOOS = linux
10+
GO = go
11+
GODOC = godoc
12+
GOFMT = gofmt
13+
DOCKER = docker
14+
TIMEOUT = 15
15+
V = 0
16+
Q = $(if $(filter 1,$V),,@)
17+
M = $(shell printf "\033[34;1m▶\033[0m")
18+
19+
export GO111MODULE=on
20+
21+
.PHONY: all
22+
all: anchor-image monkey-image
23+
24+
.PHONY: anchor-image
25+
anchor-image: anchor octopus
26+
$Q cp scripts/install-cni.sh $(BUILD)/anchor
27+
$Q $(DOCKER) build -t anchor:$(VERSION) $(BUILD)/anchor
28+
29+
.PHONY: monkey-image
30+
monkey-image: monkey
31+
$Q $(DOCKER) build -t monkey:$(VERSION) $(BUILD)/monkey
32+
33+
# TODO: this will fmt or lint twice.
34+
anchor:
35+
$Q mkdir -p $(BUILD)/anchor
36+
$Q GOOS=$(GOOS) $(GO) build -o $(BUILD)/anchor/anchor cmd/anchor/anchor.go
37+
38+
octopus:
39+
$Q mkdir -p $(BUILD)/anchor
40+
$Q GOOS=$(GOOS) $(GO) build -o $(BUILD)/anchor/octopus cmd/octopus/octopus.go
41+
42+
monkey:
43+
$Q mkdir -p $(BUILD)/monkey
44+
$Q GOOS=$(GOOS) $(GO) build -o $(BUILD)/monkey/monkey cmd/monkey/monkey.go
45+
46+
# Tools
47+
$(BIN):
48+
@mkdir -p $@
49+
50+
$(BIN)/%: | $(BIN) ; $(info $(M) building $(REPOSITORY)...)
51+
$Q tmp=$$(mktemp -d); \
52+
env GO111MODULE=off GOCACHE=off GOPATH=$$tmp GOBIN=$(BIN) $(GO) get $(REPOSITORY) \
53+
|| ret=$$?; \
54+
rm -rf $$tmp ; exit $$ret
55+
56+
GOLINT = $(BIN)/golint
57+
$(BIN)/golint: REPOSITORY=github.com/golang/lint/golint
58+
59+
GOCOVMERGE = $(BIN)/gocovmerge
60+
$(BIN)/gocovmerge: REPOSITORY=github.com/wadey/gocovmerge
61+
62+
GOCOV = $(BIN)/gocov
63+
$(BIN)/gocov: REPOSITORY=github.com/axw/gocov/...
64+
65+
GOCOVXML = $(BIN)/gocov-xml
66+
$(BIN)/gocov-xml: REPOSITORY=github.com/AlekSi/gocov-xml
67+
68+
GO2XUNIT = $(BIN)/go2xunit
69+
$(BIN)/go2xunit: REPOSITORY=github.com/tebeka/go2xunit
70+
71+
# Tests
72+
73+
TEST_TARGETS := test-default test-bench test-short test-verbose test-race
74+
.PHONY: $(TEST_TARGETS) test-xml check test tests
75+
test-bench: ARGS=-run=__absolutelynothing__ -bench=. ## Run benchmarks
76+
test-short: ARGS=-short ## Run only short tests
77+
test-verbose: ARGS=-v ## Run tests in verbose mode with coverage reporting
78+
test-race: ARGS=-race ## Run tests with race detector
79+
$(TEST_TARGETS): NAME=$(MAKECMDGOALS:test-%=%)
80+
$(TEST_TARGETS): test
81+
check test tests: fmt lint ; $(info $(M) running $(NAME:%=% )tests...) @ ## Run tests
82+
$Q $(GO) test -timeout $(TIMEOUT)s $(ARGS) $(TESTPKGS)
83+
84+
test-xml: fmt lint | $(GO2XUNIT) ; $(info $(M) running $(NAME:%=% )tests...) @ ## Run tests with xUnit output
85+
$Q mkdir -p test
86+
$Q 2>&1 $(GO) test -timeout 20s -v $(TESTPKGS) | tee test/tests.output
87+
$(GO2XUNIT) -fail -input test/tests.output -output test/tests.xml
88+
89+
COVERAGE_MODE = atomic
90+
COVERAGE_PROFILE = $(COVERAGE_DIR)/profile.out
91+
COVERAGE_XML = $(COVERAGE_DIR)/coverage.xml
92+
COVERAGE_HTML = $(COVERAGE_DIR)/index.html
93+
.PHONY: test-coverage test-coverage-tools
94+
test-coverage-tools: | $(GOCOVMERGE) $(GOCOV) $(GOCOVXML)
95+
test-coverage: COVERAGE_DIR := $(CURDIR)/test/coverage.$(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
96+
test-coverage: fmt lint test-coverage-tools ; $(info $(M) running coverage tests...) @ ## Run coverage tests
97+
$Q mkdir -p $(COVERAGE_DIR)/coverage
98+
$Q for pkg in $(TESTPKGS); do \
99+
$(GO) test \
100+
-coverpkg=$$($(GO) list -f '{{ join .Deps "\n" }}' $$pkg | \
101+
grep '^$(PACKAGE)/' | \
102+
tr '\n' ',')$$pkg \
103+
-covermode=$(COVERAGE_MODE) \
104+
-coverprofile="$(COVERAGE_DIR)/coverage/`echo $$pkg | tr "/" "-"`.cover" $$pkg ;\
105+
done
106+
$Q $(GOCOVMERGE) $(COVERAGE_DIR)/coverage/*.cover > $(COVERAGE_PROFILE)
107+
$Q $(GO) tool cover -html=$(COVERAGE_PROFILE) -o $(COVERAGE_HTML)
108+
$Q $(GOCOV) convert $(COVERAGE_PROFILE) | $(GOCOVXML) > $(COVERAGE_XML)
109+
110+
.PHONY: lint
111+
lint: | $(GOLINT) ; $(info $(M) running golint...) @ ## Run golint
112+
$Q $(GOLINT) -set_exit_status $(PKGS)
113+
114+
.PHONY: binaries
115+
binaries: | $(ANCHOR) $(OCTOPUS) $(MONKEY) ; $(info $(M) build binaries...) @ ## Building
116+
117+
.PHONY: fmt
118+
fmt: ; $(info $(M) running gofmt...) @ ## Run gofmt on all source files
119+
@ret=0 && for d in $$($(GO) list -f '{{.Dir}}' ./...); do \
120+
$(GOFMT) -l -w $$d/*.go || ret=$$? ; \
121+
done ; exit $$ret
122+
123+
# Misc
124+
125+
.PHONY: clean
126+
clean: ; $(info $(M) cleaning...) @ ## Cleanup everything
127+
@rm $(BUILD)/anchor/anchor $(BUILD)/anchor/octopus $(BUILD)/anchor/install-cni.sh
128+
@rm -rf $(BUILD)/monkey/monkey $(BUILD)/monkey/powder
129+
@rm -rf test/tests.* test/coverage.*
130+
131+
.PHONY: help
132+
help:
133+
@grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
134+
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
135+
136+
.PHONY: version
137+
version:
138+
@echo $(VERSION)

README.md

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Anchor and Octopus
2+
3+
Some applications, especially legacy applications or applications which monitor network traffic, expect to be directly connected to the physical network. In this type of situation, users hope using the macvlan network driver to assign a MAC address to each container’s virtual network interface, making it appear to be a physical network interface directly connected to the physical network.
4+
5+
## There comes anchor
6+
7+
Project anchor mainly contains four compenents, They are:
8+
9+
Anchor is an ipam plugin following the [CNI SPEC](https://github.com/containernetworking/cni/blob/master/SPEC.md).
10+
11+
Octopus is a main plugin that extends [macvlan](https://github.com/containernetworking/plugins/blob/master/plugins/main/macvlan/macvlan.go) to support multiple masters on the node. It is useful when there are multiple VLANs in the cluster.
12+
13+
Monkey is a WebUI that displays and operates the data used by anchor ipam.
14+
15+
The backstage hero is the installation script of the anchor, which configures and maintains the network interfaces of the node.
16+
17+
## CNI and Kubernetes
18+
19+
CNI(Container Network Interface), a CNCF project, consists of a specification and libraries for writing plugins to configure network interfaces in Linux containers, along with a number of supported plugins. CNI concerns itself only with network connectivity of containers and removing allocated resources when the container is deleted. Because of this focus, CNI has a wide range of support and the specification is simple to implement.
20+
21+
It is worth mentioning that kubernetes is just one of CNI runtimes, others including mesos, rkt, openshift.
22+
23+
## MacVLAN
24+
25+
MacVLAN is a Linux network driver that exposes underlay or host interfaces directly to VMs or Containers running in the host.
26+
27+
MacVLAN allows a single physical interface to have multiple MAC and ip addresses using MacVLAN sub-interfaces. MacVLAN interface is typically used for virtualization applications and each MacVLAN interface is connected to a Container or VM. Each container or VM can directly get dhcp address as the host would do. This would help customers who want Containers to be part of their traditional network with the IP addressing scheme that they already have.
28+
29+
When using MacVLAN, the containers is **NOT** reachable to the underlying host interfaces as the packages are intentionally filtered by Linux for additional isolation. This does not meet the SPEC of CNI and *service* in k8s cannot work correnctly. To work around it, we create a new MacVLAN interface and steal the IP and network traffic from the host interface by changing the route table on the node. This work is designed to be done by installation script.
30+
31+
## Installation
32+
33+
Please knowing that, most cloud providers(Amazon, Google, Aliyun) don't allow promiscuous mode, you may deploy Anchor on your own premises.
34+
35+
Recently, I have no resources(No time, no machines) to test whether anchor works well with other runtimes except kubernetes. The document below focuses on kubernetes.
36+
37+
**Prepare the cluster**
38+
39+
* Enable promiscuous mode on switch(or virtual switch)
40+
* Create a new kubernetes cluster without any CNI plugin
41+
* Reserve several IPs available for applications
42+
43+
**Install Anchor**
44+
45+
```shell
46+
curl -O https://raw.githubusercontent.com/hainesc/anchor/master/deployment/anchor.yaml
47+
```
48+
49+
Edit the anchor.yaml use your favorite editor, *L* means *Line* below.
50+
51+
* Remove L200 and lines below if the k8s cluster has not enabled RBAC.
52+
* L8, input the etcd endpoints used as the store for anchor, example at the end of the line.
53+
* L10 - L12, input the access token of the etcd, do nothing if SSL not enabled.
54+
* L18, input the choice whether or not create macvlan interface during the installation.
55+
* L22, input the cluster network information. Use semicolon(;) to seperate between items. eg, item *node01,eth0.2,10.0.2.8,10.0.2.1,24* tells install script creating a MacVLAN interface with the master *eth0.2* at the node whose hostname is *node01*, the additional info including IP of the master(*eth0.2* here), the gateway and mask of the subnet(10.0.2.1 and 24). You CAN have Multiple items for each node.
56+
57+
Save the change and run:
58+
59+
```shell
60+
kubectl apply -f anchor.yaml
61+
```
62+
63+
Wait for installation to complete, it will create a daemonset named anchor, a service account named anchor, a cluster role and a cluster role binding if RBAC enabled.
64+
65+
There are several works done by the pod which created by the daemonset on each node:
66+
67+
* Copy binary files named *anchor* and *octopus* to the node
68+
* Config and write a CNI config file named 10-anchor.conf to the node
69+
* Create MacVLAN interface(s) on the node, the interfaces created here will be removed on node restart, but when the node rejoin the k8s cluster, the daemonset recreates a pod and it will recrete the interfaces.
70+
71+
## Example
72+
73+
**Preparation**
74+
75+
There are three k-v stores used by the anchor ipam, they are:
76+
77+
| KV | Example | Explanation |
78+
|:----:|:---------:|:-------------:|
79+
| Namespace -> IPs | /anchor/ns/default -> 10.0.1.[2-9],10.0.2.8 | IPs are reserved and can be used by the namespace in the key |
80+
| Subnet -> Gateway | /anchor/gw/10.0.1.0/24 -> 10.0.1.1 | The map between subnet and its gateway |
81+
| Container -> IP | /anchor/cn/212b... -> 10.0.1.2 | The IP binding with the ContainerID |
82+
83+
At the beginning, the stores are empty, so just input some data according to the environment.
84+
85+
I have created a WebUI named [Powder monkey](https://github.com/hainesc/powder) to display and operate the k-v stores. The frontend is written in Angular and the backend written in Golang.
86+
87+
**Run example**
88+
89+
```shell
90+
curl -O https://raw.githubusercontent.com/hainesc/anchor/master/examples/anchor-2048.yaml
91+
```
92+
93+
Edit L14 and choose a subnet for it, then Run:
94+
95+
```shell
96+
kubectl apply -f anchor-2048.yaml
97+
```
98+
99+
Wait for the installation to complete, it will create a deployment named anchor-2048 and the service named anchor-2048.
100+
101+
```shell
102+
kubectl get pods -n default -o wide
103+
```
104+
will displays the IP binding with the Pod. Open you browser and enjoy the game via the IP of the pod.
105+
106+
Please describe the Pod if some errors, and refer to the log of kubelet for more details.
107+
108+
## Customized
109+
110+
Anchor uses *annotations* written in the yaml for passing customized config as you see in the example.
111+
112+
113+
| Key | Value | Explanation |
114+
|:-----:|:-------:|:-------------:|
115+
| cni.anchor.org/subnet | 10.0.1.0/24 | The Pod should be allocated an IP in the subnet |
116+
| cni.anchor.org/gateway | 10.0.1.254 | The gateway of the pod is overwritten by the customized one |
117+
| cni.anchor.org/routes | 10.88.0.0/16,10.0.1.5;10.99.1.0/24,10.0.1.7 | Add customized routes for the pod |
118+
119+
The *cni.anchor.org/subnet* is **mandatory** since anchor cannot guess an IP if it don't know which VLAN the pod in.
120+
121+
## TODO
122+
123+
* IPv6 support
124+
* K-V store redesign
125+
* Powder monkey improvement
126+
127+
## Donation
128+
129+
If you find anchor is useful and helpful, please consider making a donation.
130+
131+
* Bitcoin: 1HainescwMrDfay9cU1yWC6vc8ThZj8uAQ
132+
133+
134+
![](docs/media/alipay.jpg)

build/anchor/Dockerfile

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM alpine
2+
3+
LABEL maintainer "Haines Chan <[email protected]>"
4+
RUN apk --no-cache add iproute2
5+
6+
ADD anchor /opt/cni/bin/anchor
7+
ADD octopus /opt/cni/bin/octopus
8+
ADD install-cni.sh /install-cni.sh
9+
10+
ENV PATH=$PATH:/opt/cni/bin
11+
VOLUME /opt/cni
12+
WORKDIR /opt/cni/bin
13+
CMD ["/opt/cni/bin/anchor"]

build/monkey/Dockerfile

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FROM scratch
2+
3+
LABEL maintainer="Haines Chan <[email protected]>"
4+
ADD monkey /
5+
ADD powder /
6+
EXPOSE 80
7+
ENTRYPOINT ["/monkey"]

cmd/anchor/anchor.go

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/*
2+
* Copyright 2018 Haines Chan
3+
*
4+
* This program is free software; you can redistribute and/or modify it
5+
* under the terms of the standard MIT license. See LICENSE for more details
6+
*/
7+
8+
package main
9+
10+
import (
11+
"github.com/containernetworking/cni/pkg/skel"
12+
"github.com/containernetworking/cni/pkg/version"
13+
"github.com/hainesc/anchor/internal/app"
14+
)
15+
16+
func main() {
17+
skel.PluginMain(app.CmdAdd, app.CmdDel, version.All)
18+
}

cmd/monkey/Dockerfile

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
FROM scratch
2+
3+
LABEL maintainer="Haines Chan <[email protected]>"
4+
ADD . /
5+
EXPOSE 80
6+
ENTRYPOINT ["/monkey"]

cmd/monkey/monkey.conf

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"etcd_endpoints": "http://127.0.0.1:12379",
3+
"etcd_key_file": "/tmp/peer.key",
4+
"etcd_cert_file": "/tmp/peer.crt",
5+
"etcd_ca_cert_file": "/tmp/ca.crt"
6+
}

cmd/monkey/monkey.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2018 Haines Chan
3+
*
4+
* This program is free software; you can redistribute and/or modify it
5+
* under the terms of the standard MIT license. See LICENSE for more details
6+
*/
7+
8+
package main
9+
10+
import (
11+
// "os"
12+
"github.com/coreos/etcd/pkg/transport"
13+
"github.com/hainesc/anchor/pkg/monkey"
14+
"github.com/hainesc/anchor/pkg/store/etcd"
15+
"log"
16+
"net/http"
17+
"strings"
18+
)
19+
20+
func main() {
21+
// TODO: we should support read conf from env, then we can run it as a container and pass the conf via Environment.
22+
conf, err := monkey.LoadConf(".", "monkey.conf")
23+
if err != nil {
24+
log.Fatal(err.Error())
25+
}
26+
var store *etcd.Etcd
27+
if strings.Contains(conf.Endpoints, "https://") {
28+
tlsInfo := &transport.TLSInfo{
29+
CertFile: conf.CertFile,
30+
KeyFile: conf.KeyFile,
31+
TrustedCAFile: conf.TrustedCAFile,
32+
}
33+
tlsConfig, _ := tlsInfo.ClientConfig()
34+
store, err = etcd.NewEtcdClient("monkey", strings.Split(conf.Endpoints, ","), tlsConfig)
35+
} else {
36+
store, err = etcd.NewEtcdClientWithoutSSl("monkey", strings.Split(conf.Endpoints, ","))
37+
}
38+
defer store.Close()
39+
40+
if err != nil {
41+
log.Fatal("Failed to connect to etcd, ", err.Error())
42+
}
43+
44+
http.Handle("/", http.FileServer(http.Dir("./powder")))
45+
http.Handle("/api/v1/binding", monkey.NewInUseHandler(store))
46+
http.Handle("/api/v1/gateway", monkey.NewGatewayHandler(store))
47+
http.Handle("/api/v1/allocate", monkey.NewAllocateHandler(store))
48+
http.ListenAndServe(":8964", nil)
49+
}

0 commit comments

Comments
 (0)