├── LICENSE ├── Makefile ├── README.md ├── build ├── anchor │ └── Dockerfile └── monkey │ └── Dockerfile ├── cmd ├── anchor │ └── anchor.go ├── monkey │ ├── Dockerfile │ ├── monkey.conf │ └── monkey.go └── octopus │ └── octopus.go ├── configs ├── 10-anchor.conf └── READMEM.md ├── deployment └── anchor.yaml ├── docs └── media │ ├── alipay.jpg │ ├── anchor_topology.png │ └── anchor_workflow.png ├── examples └── anchor-2048.yaml ├── go.mod ├── go.sum ├── internal ├── app │ ├── anchor.go │ └── octopus.go └── pkg │ ├── config │ └── config.go │ └── customized │ ├── customized.go │ └── doc.go ├── pkg ├── allocator │ ├── allocator.go │ └── anchor │ │ └── anchor.go ├── monkey │ ├── config.go │ └── handler.go ├── runtime │ ├── k8s │ │ ├── k8s.go │ │ └── types.go │ └── runtime.go ├── store │ ├── etcd │ │ └── etcd.go │ └── store.go └── utils │ ├── config.go │ ├── range.go │ ├── range_set.go │ └── range_set_test.go └── scripts └── install-cni.sh /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018 Haines Chan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 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 | anchor: 34 | $Q mkdir -p $(BUILD)/anchor 35 | $Q GOOS=$(GOOS) $(GO) build -o $(BUILD)/anchor/anchor cmd/anchor/anchor.go 36 | 37 | octopus: 38 | $Q mkdir -p $(BUILD)/anchor 39 | $Q GOOS=$(GOOS) $(GO) build -o $(BUILD)/anchor/octopus cmd/octopus/octopus.go 40 | 41 | monkey: 42 | $Q mkdir -p $(BUILD)/monkey 43 | $Q GOOS=$(GOOS) $(GO) build -o $(BUILD)/monkey/monkey cmd/monkey/monkey.go 44 | 45 | # Tools 46 | $(BIN): 47 | @mkdir -p $@ 48 | 49 | $(BIN)/%: | $(BIN) ; $(info $(M) building $(REPOSITORY)...) 50 | $Q tmp=$$(mktemp -d); \ 51 | env GO111MODULE=off GOCACHE=off GOPATH=$$tmp GOBIN=$(BIN) $(GO) get $(REPOSITORY) \ 52 | || ret=$$?; \ 53 | rm -rf $$tmp ; exit $$ret 54 | 55 | GOLINT = $(BIN)/golint 56 | $(BIN)/golint: REPOSITORY=github.com/golang/lint/golint 57 | 58 | GOCOVMERGE = $(BIN)/gocovmerge 59 | $(BIN)/gocovmerge: REPOSITORY=github.com/wadey/gocovmerge 60 | 61 | GOCOV = $(BIN)/gocov 62 | $(BIN)/gocov: REPOSITORY=github.com/axw/gocov/... 63 | 64 | GOCOVXML = $(BIN)/gocov-xml 65 | $(BIN)/gocov-xml: REPOSITORY=github.com/AlekSi/gocov-xml 66 | 67 | GO2XUNIT = $(BIN)/go2xunit 68 | $(BIN)/go2xunit: REPOSITORY=github.com/tebeka/go2xunit 69 | 70 | # Tests 71 | 72 | TEST_TARGETS := test-default test-bench test-short test-verbose test-race 73 | .PHONY: $(TEST_TARGETS) test-xml check test tests 74 | test-bench: ARGS=-run=__absolutelynothing__ -bench=. ## Run benchmarks 75 | test-short: ARGS=-short ## Run only short tests 76 | test-verbose: ARGS=-v ## Run tests in verbose mode with coverage reporting 77 | test-race: ARGS=-race ## Run tests with race detector 78 | $(TEST_TARGETS): NAME=$(MAKECMDGOALS:test-%=%) 79 | $(TEST_TARGETS): test 80 | check test tests: fmt lint ; $(info $(M) running $(NAME:%=% )tests...) @ ## Run tests 81 | $Q $(GO) test -timeout $(TIMEOUT)s $(ARGS) $(TESTPKGS) 82 | 83 | test-xml: fmt lint | $(GO2XUNIT) ; $(info $(M) running $(NAME:%=% )tests...) @ ## Run tests with xUnit output 84 | $Q mkdir -p test 85 | $Q 2>&1 $(GO) test -timeout 20s -v $(TESTPKGS) | tee test/tests.output 86 | $(GO2XUNIT) -fail -input test/tests.output -output test/tests.xml 87 | 88 | COVERAGE_MODE = atomic 89 | COVERAGE_PROFILE = $(COVERAGE_DIR)/profile.out 90 | COVERAGE_XML = $(COVERAGE_DIR)/coverage.xml 91 | COVERAGE_HTML = $(COVERAGE_DIR)/index.html 92 | .PHONY: test-coverage test-coverage-tools 93 | test-coverage-tools: | $(GOCOVMERGE) $(GOCOV) $(GOCOVXML) 94 | test-coverage: COVERAGE_DIR := $(CURDIR)/test/coverage.$(shell date -u +"%Y-%m-%dT%H:%M:%SZ") 95 | test-coverage: fmt lint test-coverage-tools ; $(info $(M) running coverage tests...) @ ## Run coverage tests 96 | $Q mkdir -p $(COVERAGE_DIR)/coverage 97 | $Q for pkg in $(TESTPKGS); do \ 98 | $(GO) test \ 99 | -coverpkg=$$($(GO) list -f '{{ join .Deps "\n" }}' $$pkg | \ 100 | grep '^$(PACKAGE)/' | \ 101 | tr '\n' ',')$$pkg \ 102 | -covermode=$(COVERAGE_MODE) \ 103 | -coverprofile="$(COVERAGE_DIR)/coverage/`echo $$pkg | tr "/" "-"`.cover" $$pkg ;\ 104 | done 105 | $Q $(GOCOVMERGE) $(COVERAGE_DIR)/coverage/*.cover > $(COVERAGE_PROFILE) 106 | $Q $(GO) tool cover -html=$(COVERAGE_PROFILE) -o $(COVERAGE_HTML) 107 | $Q $(GOCOV) convert $(COVERAGE_PROFILE) | $(GOCOVXML) > $(COVERAGE_XML) 108 | 109 | .PHONY: lint 110 | lint: | $(GOLINT) ; $(info $(M) running golint...) @ ## Run golint 111 | $Q $(GOLINT) -set_exit_status $(PKGS) 112 | 113 | .PHONY: fmt 114 | fmt: ; $(info $(M) running gofmt...) @ ## Run gofmt on all source files 115 | @ret=0 && for d in $$($(GO) list -f '{{.Dir}}' ./...); do \ 116 | $(GOFMT) -l -w $$d/*.go || ret=$$? ; \ 117 | done ; exit $$ret 118 | 119 | # Misc 120 | 121 | .PHONY: clean 122 | clean: ; $(info $(M) cleaning...) @ ## Cleanup everything 123 | @rm $(BUILD)/anchor/anchor $(BUILD)/anchor/octopus $(BUILD)/anchor/install-cni.sh 124 | @rm -rf $(BUILD)/monkey/monkey $(BUILD)/monkey/powder 125 | @rm -rf test/tests.* test/coverage.* 126 | 127 | .PHONY: help 128 | help: 129 | @grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ 130 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' 131 | 132 | .PHONY: version 133 | version: 134 | @echo $(VERSION) 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anchor and Octopus 2 | 3 | In the network model of Kubernetes and many CNI plugins such as calico, it looks like that there is an assumption that each node has one and only one external network interface used to connect with outer. For Kubernetes, it does not listen to all interfaces. It picks the interface from route table with the default gateway and listens to that. And for calico, When calico/node is started, it determines the IP and subnet configuration following some stragies, after all, it uses only one network interface. Check the route table on the calico node to make sure that. 4 | 5 | However, there are some scenarios that a node has multiple external interfaces, all of them are expected to be used to connect with outer. It is very common in the data center. Of course, we can bond them all into `bond0` and calico works well over it, there are no barriers for startups since that everything are newly built, the applications, the network, the firewall, the monitor systems, etc. On the other hand, for the company that are tens of years old, applications are designed to run on an isolate LAN. The IT infrastructures are designed to isolate applications by LAN, it does not work well. 6 | 7 | In distributed system, there is a best practise that at network level, management panel should be **separated** from data panel to avoid that some applications run out of network bandwith, and the package of heart beat may be blocked by a long time, and it may run into a chaos. 8 | 9 | When the era of Kubernetes and container comes, the lack of network plugin with multiple LANs/interfaces support blocks them from running legacy applications on Kubernetes. 10 | 11 | ## There come anchor and octopus 12 | 13 | Anchor is a Layer-2 CNI plugin based MacVLAN with multiple LANs/interfaces support. A new born container is attached to a special network interface, based on the LAN which it desires to be in. And via that interface, the container can connect with gateway, it is little different from adding a node to a given LAN, there is no NAT, no tunnel. 14 | 15 | For that purpose, it is straightful that the plugin should work at Layer-2 of network model. Newtork virtualization technique such as **Bridge** and **MacVLAN** can help us attaching the container to network interface. We prefer **MacVLAN** over Bridge for its perfermance. 16 | 17 | By using MacVLAN network driver, It can 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/gateway. 18 | 19 | See the picture below which shows the network topology of a kubernetes cluster with Anchor as its CNI plugin. 20 | 21 | ![](docs/media/anchor_topology.png) 22 | 23 | Project anchor mainly contains four components, They are: 24 | 25 | * Anchor is an IPAM plugin following the [CNI SPEC](https://github.com/containernetworking/cni/blob/master/SPEC.md). 26 | 27 | * Octopus is a main plugin that extends [macvlan](https://github.com/containernetworking/plugins/blob/master/plugins/main/macvlan/macvlan.go) to support multiple network interfaces on the node. It is useful when there are multiple VLANs in the cluster. 28 | 29 | * Monkey is a WebUI that displays and operates the data used by anchor IPAM. 30 | 31 | * The backstage hero is the installation script of the anchor, which configures and maintains the network interfaces of the node. 32 | 33 | ## CNI and Kubernetes 34 | 35 | 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. 36 | 37 | It is worth mentioning that kubernetes is just one of CNI runtimes, others including mesos, rkt, openshift, etc. 38 | 39 | The work flow of anchor running inside a kubernetes cluster shown as the picture below. 40 | 41 | ![](docs/media/anchor_workflow.png) 42 | 43 | ## MacVLAN 44 | 45 | MacVLAN is a Linux network driver that exposes underlay or host interfaces directly to VMs or Containers running in the host. 46 | 47 | MacVLAN allows a single physical interface to have multiple MACs 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 or IPAM 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. 48 | 49 | 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 causes *service* in k8s cannot work correnctly. To work around it, we create a new MacVLAN interface named *acr1* as shown by the topology, the interface steals the IP and network traffic from the host interface by changing the route table in the host. This work is designed to be done by installation script. 50 | 51 | ## Installation 52 | 53 | Please knowing that, most cloud providers(Amazon, Google, Aliyun) don't allow promiscuous mode, you may deploy Anchor on your own premises. 54 | 55 | 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. 56 | 57 | **Prepare the cluster** 58 | 59 | * Enable promiscuous mode on switch(or virtual switch) 60 | * Create a new kubernetes cluster without any CNI plugin 61 | * Reserve several IPs available for applications 62 | 63 | **Install Anchor** 64 | 65 | ```shell 66 | curl -O https://raw.githubusercontent.com/hainesc/anchor/master/deployment/anchor.yaml 67 | ``` 68 | 69 | Edit the anchor.yaml using your favorite editor, *L* means *Line* below. 70 | 71 | * Remove L200 and lines below if the k8s cluster has not enabled RBAC. 72 | * L8, input the etcd endpoints used as the store by anchor, see example at the end of the line. 73 | * L10 - L12, input the access tokens of the etcd, remove if SSL not enabled. 74 | * L18, input the choice whether or not create macvlan interface during the installation. 75 | * 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. 76 | 77 | Save the change and run: 78 | 79 | ```shell 80 | kubectl apply -f anchor.yaml 81 | ``` 82 | 83 | 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. 84 | 85 | There are several works done by the pod which created by the daemonset on each node: 86 | 87 | * Copy binary files named *anchor* and *octopus* to the node 88 | * Config and write a CNI config file named 10-anchor.conf to the node 89 | * 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. 90 | 91 | ## Run an example 92 | 93 | **Preparation** 94 | 95 | Before the example, we should initialize the etcd store used by anchor. 96 | 97 | There are three k-v stores used by the anchor ipam, they are: 98 | 99 | | KV | Example | Explanation | 100 | |:----:|:---------:|:-------------:| 101 | | 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 | 102 | | Subnet -> Gateway | /anchor/gw/10.0.1.0/24 -> 10.0.1.1 | The map between subnet and its gateway | 103 | | Container -> IP | /anchor/cn/212b... -> 10.0.1.2 | The IP binding with the ContainerID | 104 | 105 | At the beginning, the stores are empty, so just input some data following the environment. 106 | 107 | Make sure **export ETCDCTL_API=3** before run etcd cli, since Anchor uses etcd v3. 108 | 109 | 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. It is not beautiful since I am newbie to Angular but it works well. 110 | 111 | **Run example** 112 | 113 | ```shell 114 | curl -O https://raw.githubusercontent.com/hainesc/anchor/master/examples/anchor-2048.yaml 115 | ``` 116 | 117 | Edit L14 and choose a subnet for it, then Run: 118 | 119 | ```shell 120 | kubectl apply -f anchor-2048.yaml 121 | ``` 122 | 123 | Wait for the installation to complete, it will create a deployment named anchor-2048 and a service named anchor-2048. 124 | 125 | ```shell 126 | kubectl get pods -n default -o wide 127 | ``` 128 | will displays the IP binding with the Pod. Open you browser and enjoy the game via the IP of the pod. 129 | 130 | Please describe the Pod if some errors, and refer to the log of kubelet for more details. 131 | 132 | ## Customized 133 | 134 | Anchor uses *annotations* written in the yaml for passing customized config as you see in the example. 135 | 136 | 137 | | Key | Value | Explanation | 138 | |:-----:|:-------:|:-------------:| 139 | | cni.anchor.org/subnet | 10.0.1.0/24 | The Pod should be allocated an IP in the subnet | 140 | | cni.anchor.org/gateway | 10.0.1.254 | The gateway of the pod is overwritten by the customized one | 141 | | 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 | 142 | 143 | The *cni.anchor.org/subnet* is **mandatory** since anchor cannot guess an IP if it don't know which VLAN the pod in. 144 | 145 | ## Known Users 146 | 147 | Please let me know by posting a pull request with the logo of your company if you are using Anchor. 148 | 149 | ## TODO 150 | 151 | * IPv6 support 152 | * K-V store redesign 153 | * Powder monkey improvement 154 | 155 | ## Donation 156 | 157 | If you find anchor is useful and helpful, please consider making a donation. 158 | 159 | * Bitcoin: bc1qm0gxtwrzgqqmfsjqn8w37qc9hvdkva8xuxa00m 160 | * Alipay: zhinhai@gmail.com 161 | 162 | ![](docs/media/alipay.jpg) 163 | -------------------------------------------------------------------------------- /build/anchor/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | LABEL maintainer "Haines Chan " 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"] 14 | -------------------------------------------------------------------------------- /build/monkey/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | LABEL maintainer="Haines Chan " 4 | ADD monkey / 5 | ADD powder / 6 | EXPOSE 80 7 | ENTRYPOINT ["/monkey"] 8 | -------------------------------------------------------------------------------- /cmd/anchor/anchor.go: -------------------------------------------------------------------------------- 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 | } 19 | -------------------------------------------------------------------------------- /cmd/monkey/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | LABEL maintainer="Haines Chan " 4 | ADD . / 5 | EXPOSE 80 6 | ENTRYPOINT ["/monkey"] 7 | -------------------------------------------------------------------------------- /cmd/monkey/monkey.conf: -------------------------------------------------------------------------------- 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 | } 7 | -------------------------------------------------------------------------------- /cmd/monkey/monkey.go: -------------------------------------------------------------------------------- 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 | } 50 | -------------------------------------------------------------------------------- /cmd/octopus/octopus.go: -------------------------------------------------------------------------------- 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 | "errors" 12 | "fmt" 13 | "net" 14 | "runtime" 15 | 16 | "github.com/containernetworking/cni/pkg/skel" 17 | "github.com/containernetworking/cni/pkg/types" 18 | "github.com/containernetworking/cni/pkg/types/current" 19 | "github.com/containernetworking/cni/pkg/version" 20 | "github.com/containernetworking/plugins/pkg/ip" 21 | "github.com/containernetworking/plugins/pkg/ipam" 22 | 23 | "github.com/hainesc/anchor/internal/app" 24 | "github.com/hainesc/anchor/internal/pkg/config" 25 | "github.com/hainesc/anchor/pkg/runtime/k8s" 26 | 27 | "github.com/containernetworking/plugins/pkg/ns" 28 | "github.com/containernetworking/plugins/pkg/utils/sysctl" 29 | "github.com/j-keck/arping" 30 | "github.com/vishvananda/netlink" 31 | ) 32 | 33 | const ( 34 | // IPv4InterfaceArpProxySysctlTemplate represents a template for format output. 35 | IPv4InterfaceArpProxySysctlTemplate = "net.ipv4.conf.%s.proxy_arp" 36 | ) 37 | 38 | func init() { 39 | // this ensures that main runs only on main thread (thread group leader). 40 | // since namespace ops (unshare, setns) are done for a single thread, we 41 | // must ensure that the goroutine does not jump from OS thread to thread 42 | runtime.LockOSThread() 43 | } 44 | 45 | func createMacvlan(conf *config.OctopusConf, ifName string, netns ns.NetNS, master string) (*current.Interface, error) { 46 | macvlan := ¤t.Interface{} 47 | 48 | mode, err := app.ModeFromString(conf.Mode) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | m, err := netlink.LinkByName(master) 54 | if err != nil { 55 | return nil, fmt.Errorf("failed to lookup master %q: %v", master, err) 56 | } 57 | 58 | // due to kernel bug we have to create with tmpName or it might 59 | // collide with the name on the host and error out 60 | tmpName, err := ip.RandomVethName() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | mv := &netlink.Macvlan{ 66 | LinkAttrs: netlink.LinkAttrs{ 67 | MTU: conf.MTU, 68 | Name: tmpName, 69 | ParentIndex: m.Attrs().Index, 70 | Namespace: netlink.NsFd(int(netns.Fd())), 71 | }, 72 | Mode: mode, 73 | } 74 | 75 | if err := netlink.LinkAdd(mv); err != nil { 76 | return nil, fmt.Errorf("failed to create macvlan: %v", err) 77 | } 78 | 79 | err = netns.Do(func(_ ns.NetNS) error { 80 | // TODO: duplicate following lines for ipv6 support, when it will be added in other places 81 | ipv4SysctlValueName := fmt.Sprintf(IPv4InterfaceArpProxySysctlTemplate, tmpName) 82 | if _, err := sysctl.Sysctl(ipv4SysctlValueName, "1"); err != nil { 83 | // remove the newly added link and ignore errors, because we already are in a failed state 84 | _ = netlink.LinkDel(mv) 85 | return fmt.Errorf("failed to set proxy_arp on newly added interface %q: %v", tmpName, err) 86 | } 87 | 88 | err := ip.RenameLink(tmpName, ifName) 89 | if err != nil { 90 | _ = netlink.LinkDel(mv) 91 | return fmt.Errorf("failed to rename macvlan to %q: %v", ifName, err) 92 | } 93 | macvlan.Name = ifName 94 | 95 | // Re-fetch macvlan to get all properties/attributes 96 | contMacvlan, err := netlink.LinkByName(ifName) 97 | if err != nil { 98 | return fmt.Errorf("failed to refetch macvlan %q: %v", ifName, err) 99 | } 100 | macvlan.Mac = contMacvlan.Attrs().HardwareAddr.String() 101 | macvlan.Sandbox = netns.Path() 102 | 103 | return nil 104 | }) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | return macvlan, nil 110 | } 111 | 112 | func cmdAdd(args *skel.CmdArgs) error { 113 | // n, cniVersion, err := loadConf(args.StdinData) 114 | n, cniVersion, err := config.LoadOctopusConf(args.StdinData) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | netns, err := ns.GetNS(args.Netns) 120 | if err != nil { 121 | return fmt.Errorf("failed to open netns %q: %v", netns, err) 122 | } 123 | defer netns.Close() 124 | 125 | // Get annotations of the pod and decide which host interface will be used. 126 | // 1. Get conf for k8s client and create a k8s_client 127 | k8sClient, err := k8s.NewK8sClient(n.Kubernetes, n.Policy) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | // 2. Get K8S_POD_NAME and K8S_POD_NAMESPACE. 133 | k8sArgs := k8s.Args{} 134 | if err := types.LoadArgs(args.Args, &k8sArgs); err != nil { 135 | return err 136 | } 137 | 138 | // 3. Get annotations from k8s_client via K8S_POD_NAME and K8S_POD_NAMESPACE. 139 | _, annot, err := k8s.GetK8sPodInfo(k8sClient, string(k8sArgs.K8S_POD_NAME), string(k8sArgs.K8S_POD_NAMESPACE)) 140 | if err != nil { 141 | return fmt.Errorf("failed to read annotaions for pod " + err.Error()) 142 | } 143 | subnet := annot["cni.anchor.org/subnet"] 144 | 145 | master := n.Octopus[subnet] 146 | if master == "" { 147 | if subnet == "" { 148 | return fmt.Errorf("failed to find annotation named cni.anchor.org/subnet") 149 | } 150 | return fmt.Errorf("Master interface not found for VLAN %s on this node", subnet) 151 | } 152 | macvlanInterface, err := createMacvlan(n, args.IfName, netns, master) 153 | if err != nil { 154 | return err 155 | } 156 | 157 | // Delete link if err to avoid link leak in this ns 158 | defer func() { 159 | if err != nil { 160 | netns.Do(func(_ ns.NetNS) error { 161 | return ip.DelLinkByName(args.IfName) 162 | }) 163 | } 164 | }() 165 | 166 | // run the IPAM plugin and get back the config to apply 167 | r, err := ipam.ExecAdd(n.IPAM.Type, args.StdinData) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | // Invoke ipam del if err to avoid ip leak 173 | defer func() { 174 | if err != nil { 175 | ipam.ExecDel(n.IPAM.Type, args.StdinData) 176 | } 177 | }() 178 | 179 | // Convert whatever the IPAM result was into the current Result type 180 | result, err := current.NewResultFromResult(r) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | if len(result.IPs) == 0 { 186 | return errors.New("IPAM plugin returned missing IP config") 187 | } 188 | result.Interfaces = []*current.Interface{macvlanInterface} 189 | 190 | for _, ipc := range result.IPs { 191 | // All addresses apply to the container macvlan interface 192 | ipc.Interface = current.Int(0) 193 | } 194 | 195 | err = netns.Do(func(_ ns.NetNS) error { 196 | if err := ipam.ConfigureIface(args.IfName, result); err != nil { 197 | return err 198 | } 199 | 200 | contVeth, err := net.InterfaceByName(args.IfName) 201 | if err != nil { 202 | return fmt.Errorf("failed to look up %q: %v", args.IfName, err) 203 | } 204 | 205 | for _, ipc := range result.IPs { 206 | if ipc.Version == "4" { 207 | _ = arping.GratuitousArpOverIface(ipc.Address.IP, *contVeth) 208 | } 209 | } 210 | return nil 211 | }) 212 | if err != nil { 213 | return err 214 | } 215 | 216 | result.DNS = n.DNS 217 | 218 | return types.PrintResult(result, cniVersion) 219 | } 220 | 221 | func cmdDel(args *skel.CmdArgs) error { 222 | n, _, err := config.LoadOctopusConf(args.StdinData) 223 | if err != nil { 224 | return err 225 | } 226 | 227 | err = ipam.ExecDel(n.IPAM.Type, args.StdinData) 228 | if err != nil { 229 | return err 230 | } 231 | 232 | if args.Netns == "" { 233 | return nil 234 | } 235 | 236 | // There is a netns so try to clean up. Delete can be called multiple times 237 | // so don't return an error if the device is already removed. 238 | err = ns.WithNetNSPath(args.Netns, func(_ ns.NetNS) error { 239 | if err := ip.DelLinkByName(args.IfName); err != nil { 240 | if err != ip.ErrLinkNotFound { 241 | return err 242 | } 243 | } 244 | return nil 245 | }) 246 | 247 | return err 248 | } 249 | 250 | func main() { 251 | skel.PluginMain(cmdAdd, cmdDel, version.All) 252 | } 253 | -------------------------------------------------------------------------------- /configs/10-anchor.conf: -------------------------------------------------------------------------------- 1 | { 2 | "name": "anchor", 3 | "cniVersion": "0.3.1", 4 | "type": "octopus", 5 | "octopus": {"192.168.0.0/16":"ens192"}, 6 | "policy": { 7 | "type": "k8s", 8 | "k8s_api_root": "https://10.96.0.1:443", 9 | "k8s_auth_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhbmNob3ItdG9rZW4tenJ3OWMiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiYW5jaG9yIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiNzRkZjI4MTUtODY0Yy0xMWU4LThjNjctMDI0MmFjMTIwMDAyIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Omt1YmUtc3lzdGVtOmFuY2hvciJ9.h-urzdX0bdH9ZeFnXkivfKHX0oxnvxLdNvh3yFXvNU_Sa0rDEuaR8HtA9L1Cquj_pscNt7_cT1giqsV3085l9INsA6HfldsJjleVZclo3V3uuZ3hmOMl7CDFXvxM6sfV2gfTe-mrCyjBV0AO4g9rNEqIpOCrBztNTuEwguY5gwbPV9RGUUelv6FW-64Cgp-IJMjSmLkU3W61Pvqo3Xdwcken4j_h-96a9fnfsww4hGcyZcdgJyP05zrPDo77fKzHkaNSLZ_-7A33kRUcQMAktJl_SdEaM8_a-LqixHw7oSklqYryhFB3GO2cpNV_gLcJIPyuJ3AIhgQ6y7cmChSOJi1sFjXSCgfKORoGoMAfSDkDClhRWajp0i34OeSfbmrLkFyNzJYACgAx3yn6uk_AjCeoQAdG02ldwI0DHgQni2jqUxT3V0zW-Jc7L5C10tnCapXhoJ9uEZBypFghNVokCy8Ff4zRD7P3glyeY6b2x00dKDxqUVDec5MQdpY4pKof6pXvnZmr6x-aNcj_6ehSIh89HF2E0AdGCBbGzMki6zn-zPpUOE24Q4EF7_3F4whU2UYUcEN0OyRFnXu1H_pKFYL6sKgDk2NshWPJ2XfrnXMgN615pwmogpbgpSSPyNLtjJVJtPvyoEFDElmNgQhPupIR5piO-A83eaGZ0jeOADc" 10 | }, 11 | "kubernetes": { 12 | "kubeconfig": "/etc/cni/net.d/anchor-kubeconfig" 13 | }, 14 | "ipam": { 15 | "type": "anchor-ipam", 16 | "etcd_endpoints": "https://192.168.100.86:12379", 17 | "etcd_key_file": "/etc/cni/net.d/calico-tls/etcd-key", 18 | "etcd_cert_file": "/etc/cni/net.d/calico-tls/etcd-cert", 19 | "etcd_ca_cert_file": "/etc/cni/net.d/calico-tls/etcd-ca", 20 | "service_ipnet": "10.96.0.0/12", 21 | "node_ips": ["192.168.100.86"], 22 | "policy": { 23 | "type": "k8s", 24 | "k8s_api_root": "https://10.96.0.1:443", 25 | "k8s_auth_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhbmNob3ItdG9rZW4tenJ3OWMiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiYW5jaG9yIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiNzRkZjI4MTUtODY0Yy0xMWU4LThjNjctMDI0MmFjMTIwMDAyIiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50Omt1YmUtc3lzdGVtOmFuY2hvciJ9.h-urzdX0bdH9ZeFnXkivfKHX0oxnvxLdNvh3yFXvNU_Sa0rDEuaR8HtA9L1Cquj_pscNt7_cT1giqsV3085l9INsA6HfldsJjleVZclo3V3uuZ3hmOMl7CDFXvxM6sfV2gfTe-mrCyjBV0AO4g9rNEqIpOCrBztNTuEwguY5gwbPV9RGUUelv6FW-64Cgp-IJMjSmLkU3W61Pvqo3Xdwcken4j_h-96a9fnfsww4hGcyZcdgJyP05zrPDo77fKzHkaNSLZ_-7A33kRUcQMAktJl_SdEaM8_a-LqixHw7oSklqYryhFB3GO2cpNV_gLcJIPyuJ3AIhgQ6y7cmChSOJi1sFjXSCgfKORoGoMAfSDkDClhRWajp0i34OeSfbmrLkFyNzJYACgAx3yn6uk_AjCeoQAdG02ldwI0DHgQni2jqUxT3V0zW-Jc7L5C10tnCapXhoJ9uEZBypFghNVokCy8Ff4zRD7P3glyeY6b2x00dKDxqUVDec5MQdpY4pKof6pXvnZmr6x-aNcj_6ehSIh89HF2E0AdGCBbGzMki6zn-zPpUOE24Q4EF7_3F4whU2UYUcEN0OyRFnXu1H_pKFYL6sKgDk2NshWPJ2XfrnXMgN615pwmogpbgpSSPyNLtjJVJtPvyoEFDElmNgQhPupIR5piO-A83eaGZ0jeOADc" 26 | }, 27 | "kubernetes": { 28 | "kubeconfig": "/etc/cni/net.d/anchor-kubeconfig" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /configs/READMEM.md: -------------------------------------------------------------------------------- 1 | # Configs 2 | 3 | Configuration file templates or default configs here. 4 | -------------------------------------------------------------------------------- /deployment/anchor.yaml: -------------------------------------------------------------------------------- 1 | kind: ConfigMap 2 | apiVersion: v1 3 | metadata: 4 | name: anchor-config 5 | namespace: kube-system 6 | data: 7 | # Configure this with the location of your etcd cluster. 8 | etcd_endpoints: "" # "https://10.0.1.2:2379,https://10.0.1.3:2379" 9 | # Configure the Secret below if etcd TLS enabled. 10 | etcd_ca: "" # "/anchor-secrets/etcd-ca" 11 | etcd_cert: "" # "/anchor-secrets/etcd-cert" 12 | etcd_key: "" # "/anchor-secrets/etcd-key" 13 | 14 | # Config this as the value you specify in kube-apiserver. 15 | service_cluster_ip_range: "" # "10.96.0.0/12" 16 | anchor_mode: "macvlan" # "macvlan or octopus" 17 | # Create a macvlan interface at the node, Only centos 7.2+ tested. 18 | create_macvlan: "" # "true" or "false" 19 | # Configure it only when create_macvlan is true. 20 | # Fields: hostname,master_interface,ip,gateway,mask. Use semicolon(;) 21 | # to seperate multi items. Recently we only support one item per node 22 | cluster_network: "" # "node01,eth0,10.0.2.8,10.0.2.1,24;node02,eth3.2,10.0.12.3,10.0.12.1,24" 23 | # The CNI network configuration to install on each node. 24 | cni_network_config: |- 25 | { 26 | "name": "anchor", 27 | "cniVersion": "0.3.1", 28 | "type": "__ANCHOR_MODE__", 29 | "master": "__MACVLAN_INTERFACE__", 30 | "octopus": {__OCTOPUS__}, 31 | "policy": { 32 | "type": "k8s", 33 | "k8s_api_root": "https://__KUBERNETES_SERVICE_HOST__:__KUBERNETES_SERVICE_PORT__", 34 | "k8s_auth_token": "__SERVICEACCOUNT_TOKEN__" 35 | }, 36 | "kubernetes": { 37 | "kubeconfig": "__KUBECONFIG_FILEPATH__" 38 | }, 39 | "ipam": { 40 | "type": "anchor", 41 | "etcd_endpoints": "__ETCD_ENDPOINTS__", 42 | "etcd_key_file": "__ETCD_KEY_FILE__", 43 | "etcd_cert_file": "__ETCD_CERT_FILE__", 44 | "etcd_ca_cert_file": "__ETCD_CA_CERT_FILE__", 45 | "service_ipnet": "__SERVICE_CLUSTER_IP_RANGE__", 46 | "node_ips": [__NODE_IPS__], 47 | "policy": { 48 | "type": "k8s", 49 | "k8s_api_root": "https://__KUBERNETES_SERVICE_HOST__:__KUBERNETES_SERVICE_PORT__", 50 | "k8s_auth_token": "__SERVICEACCOUNT_TOKEN__" 51 | }, 52 | "kubernetes": { 53 | "kubeconfig": "__KUBECONFIG_FILEPATH__" 54 | } 55 | } 56 | } 57 | 58 | --- 59 | 60 | # The following contains k8s Secrets for use with a TLS enabled etcd cluster. 61 | # For information on populating Secrets, see http://kubernetes.io/docs/user-guide/secrets/ 62 | apiVersion: v1 63 | kind: Secret 64 | type: Opaque 65 | metadata: 66 | name: anchor-etcd-secrets 67 | namespace: kube-system 68 | data: 69 | # Populate the following files with etcd TLS configuration if desired, but leave blank if 70 | # not using TLS for etcd. 71 | # This self-hosted install expects three files with the following names. The values 72 | # should be base64 encoded strings of the entire contents of each file. 73 | # etcd-key: null 74 | # etcd-cert: null 75 | # etcd-ca: null 76 | 77 | --- 78 | 79 | # This manifest installs the anchor container, which install CNI plugins on each node. 80 | kind: DaemonSet 81 | apiVersion: extensions/v1beta1 82 | metadata: 83 | name: anchor 84 | namespace: kube-system 85 | labels: 86 | k8s-app: anchor 87 | spec: 88 | selector: 89 | matchLabels: 90 | k8s-app: anchor 91 | template: 92 | metadata: 93 | labels: 94 | k8s-app: anchor 95 | spec: 96 | hostNetwork: true 97 | serviceAccountName: anchor 98 | # restartPolicy: Never 99 | # Minimize downtime during a rolling upgrade or deletion; tell Kubernetes to do a "force 100 | # deletion": https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods. 101 | terminationGracePeriodSeconds: 0 102 | containers: 103 | # This container installs the anchor CNI binaries 104 | # and CNI network config file on each node. 105 | - name: anchor-install 106 | image: docker.io/hainesc/anchor:v0.4.0 107 | command: ["/install-cni.sh"] 108 | securityContext: 109 | capabilities: 110 | add: 111 | - NET_ADMIN 112 | env: 113 | # The location of the Anchor etcd cluster. 114 | - name: ETCD_ENDPOINTS 115 | valueFrom: 116 | configMapKeyRef: 117 | name: anchor-config 118 | key: etcd_endpoints 119 | 120 | - name: CREATE_MACVLAN 121 | valueFrom: 122 | configMapKeyRef: 123 | name: anchor-config 124 | key: create_macvlan 125 | - name: CLUSTER_NETWORK 126 | valueFrom: 127 | configMapKeyRef: 128 | name: anchor-config 129 | key: cluster_network 130 | 131 | # ETCD cert and key 132 | - name: ETCD_CA 133 | valueFrom: 134 | configMapKeyRef: 135 | name: anchor-config 136 | key: etcd_ca 137 | - name: ETCD_CERT 138 | valueFrom: 139 | configMapKeyRef: 140 | name: anchor-config 141 | key: etcd_cert 142 | - name: ETCD_KEY 143 | valueFrom: 144 | configMapKeyRef: 145 | name: anchor-config 146 | key: etcd_key 147 | # Service cluster ip range 148 | - name: SERVICE_CLUSTER_IP_RANGE 149 | valueFrom: 150 | configMapKeyRef: 151 | name: anchor-config 152 | key: service_cluster_ip_range 153 | 154 | - name: ANCHOR_MODE 155 | valueFrom: 156 | configMapKeyRef: 157 | name: anchor-config 158 | key: anchor_mode 159 | 160 | # The CNI network config to install on each node. 161 | - name: CNI_NETWORK_CONFIG 162 | valueFrom: 163 | configMapKeyRef: 164 | name: anchor-config 165 | key: cni_network_config 166 | volumeMounts: 167 | - mountPath: /host/opt/cni/bin 168 | name: cni-bin-dir 169 | - mountPath: /host/etc/cni/net.d 170 | name: cni-net-dir 171 | - mountPath: /anchor-secrets 172 | name: etcd-certs 173 | tolerations: 174 | - effect: NoSchedule 175 | key: node-role.kubernetes.io/master 176 | - effect: NoSchedule 177 | key: node.cloudprovider.kubernetes.io/uninitialized 178 | value: "true" 179 | volumes: 180 | # Used to install CNI. 181 | - name: cni-bin-dir 182 | hostPath: 183 | path: /opt/cni/bin 184 | - name: cni-net-dir 185 | hostPath: 186 | path: /etc/cni/net.d 187 | # Mount in the etcd TLS secrets. 188 | - name: etcd-certs 189 | secret: 190 | secretName: anchor-etcd-secrets 191 | 192 | --- 193 | 194 | apiVersion: v1 195 | kind: ServiceAccount 196 | metadata: 197 | name: anchor 198 | namespace: kube-system 199 | 200 | --- 201 | 202 | apiVersion: rbac.authorization.k8s.io/v1beta1 203 | kind: ClusterRoleBinding 204 | metadata: 205 | name: anchor 206 | roleRef: 207 | apiGroup: rbac.authorization.k8s.io 208 | kind: ClusterRole 209 | name: anchor 210 | subjects: 211 | - kind: ServiceAccount 212 | name: anchor 213 | namespace: kube-system 214 | 215 | --- 216 | 217 | kind: ClusterRole 218 | apiVersion: rbac.authorization.k8s.io/v1beta1 219 | metadata: 220 | name: anchor 221 | rules: 222 | - apiGroups: [""] 223 | resources: 224 | - pods 225 | - nodes 226 | verbs: 227 | - get 228 | - apiGroups: ["apps"] 229 | resources: 230 | - replicasets 231 | verbs: 232 | - get 233 | -------------------------------------------------------------------------------- /docs/media/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hainesc/anchor/5f4972fe88186a9a05e0eaa1b62388eb3589c762/docs/media/alipay.jpg -------------------------------------------------------------------------------- /docs/media/anchor_topology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hainesc/anchor/5f4972fe88186a9a05e0eaa1b62388eb3589c762/docs/media/anchor_topology.png -------------------------------------------------------------------------------- /docs/media/anchor_workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hainesc/anchor/5f4972fe88186a9a05e0eaa1b62388eb3589c762/docs/media/anchor_workflow.png -------------------------------------------------------------------------------- /examples/anchor-2048.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1beta1 2 | kind: Deployment 3 | metadata: 4 | name: anchor-2048 5 | labels: 6 | app: anchor-2048 7 | spec: 8 | template: 9 | metadata: 10 | name: anchor-2048 11 | labels: 12 | app: anchor-2048 13 | annotations: 14 | cni.anchor.org/subnet: 10.0.1.0/24 15 | spec: 16 | containers: 17 | - name: anchor-2048 18 | image: alexwhen/docker-2048 19 | ports: 20 | - containerPort: 80 21 | --- 22 | apiVersion: v1 23 | kind: Service 24 | metadata: 25 | name: anchor-2048 26 | spec: 27 | type: NodePort 28 | ports: 29 | - port: 80 30 | selector: 31 | app: anchor-2048 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hainesc/anchor 2 | 3 | require ( 4 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect 5 | github.com/boltdb/bolt v1.3.1 // indirect 6 | github.com/containernetworking/cni v0.6.0 7 | github.com/containernetworking/plugins v0.7.4 8 | github.com/coreos/bbolt v1.3.0 // indirect 9 | github.com/coreos/etcd v3.3.9+incompatible 10 | github.com/coreos/go-iptables v0.4.0 // indirect 11 | github.com/coreos/go-semver v0.2.0 // indirect 12 | github.com/coreos/go-systemd v0.0.0-20180828140353-eee3db372b31 // indirect 13 | github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea // indirect 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 16 | github.com/ghodss/yaml v1.0.0 // indirect 17 | github.com/gogo/protobuf v1.1.1 // indirect 18 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect 19 | github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7 // indirect 20 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect 21 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect 22 | github.com/googleapis/gnostic v0.2.0 // indirect 23 | github.com/gorilla/websocket v1.4.0 // indirect 24 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 25 | github.com/grpc-ecosystem/grpc-gateway v1.5.0 // indirect 26 | github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c // indirect 27 | github.com/imdario/mergo v0.3.6 // indirect 28 | github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56 29 | github.com/jonboulle/clockwork v0.1.0 // indirect 30 | github.com/json-iterator/go v1.1.5 // indirect 31 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect 34 | github.com/onsi/gomega v1.4.2 // indirect 35 | github.com/pmezard/go-difflib v1.0.0 // indirect 36 | github.com/prometheus/client_golang v0.8.0 // indirect 37 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect 38 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e // indirect 39 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273 // indirect 40 | github.com/sirupsen/logrus v1.0.6 // indirect 41 | github.com/soheilhy/cmux v0.1.4 // indirect 42 | github.com/spf13/pflag v1.0.2 // indirect 43 | github.com/stretchr/testify v1.2.2 // indirect 44 | github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6 // indirect 45 | github.com/ugorji/go/codec v0.0.0-20180918125716-ed9a3b5f078b // indirect 46 | github.com/vishvananda/netlink v1.0.0 47 | github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc // indirect 48 | github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect 49 | golang.org/x/crypto v0.0.0-20180816225734-aabede6cba87 // indirect 50 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 // indirect 51 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 // indirect 52 | google.golang.org/grpc v1.14.0 // indirect 53 | gopkg.in/airbrake/gobrake.v2 v2.0.9 // indirect 54 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 // indirect 55 | gopkg.in/inf.v0 v0.9.1 // indirect 56 | k8s.io/api v0.0.0-20180817212534-2315b41a07e8 // indirect 57 | k8s.io/apimachinery v0.0.0-20180228050457-302974c03f7e 58 | k8s.io/client-go v7.0.0+incompatible 59 | ) 60 | 61 | go 1.13 62 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 2 | github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= 3 | github.com/containernetworking/cni v0.6.0 h1:FXICGBZNMtdHlW65trpoHviHctQD3seWhRRcqp2hMOU= 4 | github.com/containernetworking/cni v0.6.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= 5 | github.com/containernetworking/plugins v0.7.4 h1:ugkuXfg1Pdzm54U5DGMzreYIkZPSCmSq4rm5TIXVICA= 6 | github.com/containernetworking/plugins v0.7.4/go.mod h1:dagHaAhNjXjT9QYOklkKJDGaQPTg4pf//FrUcJeb7FU= 7 | github.com/coreos/bbolt v1.3.0/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 8 | github.com/coreos/etcd v3.3.9+incompatible h1:iKSVPXGNGqroBx4+RmUXv8emeU7y+ucRZSzTYgzLZwM= 9 | github.com/coreos/etcd v3.3.9+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 10 | github.com/coreos/go-iptables v0.4.0 h1:wh4UbVs8DhLUbpyq97GLJDKrQMjEDD63T1xE4CrsKzQ= 11 | github.com/coreos/go-iptables v0.4.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= 12 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 13 | github.com/coreos/go-systemd v0.0.0-20180828140353-eee3db372b31/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 14 | github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 17 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 18 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 19 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 20 | github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= 21 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 22 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 23 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 24 | github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 25 | github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= 26 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 27 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 28 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf h1:+RRA9JqSOZFfKrOeqr2z77+8R2RKyh8PG66dcu1V0ck= 29 | github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= 30 | github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g= 31 | github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= 32 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 33 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 34 | github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= 35 | github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c h1:kQWxfPIHVLbgLzphqk3QUflDy9QdksZR4ygR807bpy0= 36 | github.com/howeyc/gopass v0.0.0-20170109162249-bf9dde6d0d2c/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= 37 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 38 | github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= 39 | github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 40 | github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56 h1:742eGXur0715JMq73aD95/FU0XpVKXqNuTnEfXsLOYQ= 41 | github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= 42 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 43 | github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= 44 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 45 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 46 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 48 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 49 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 50 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 51 | github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 52 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 53 | github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 54 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 55 | github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 56 | github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 57 | github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= 58 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 59 | github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc= 60 | github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 61 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 62 | github.com/tmc/grpc-websocket-proxy v0.0.0-20171017195756-830351dc03c6/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 63 | github.com/ugorji/go/codec v0.0.0-20180918125716-ed9a3b5f078b/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 64 | github.com/vishvananda/netlink v1.0.0 h1:bqNY2lgheFIu1meHUFSH3d7vG93AFyqg3oGbJCOJgSM= 65 | github.com/vishvananda/netlink v1.0.0/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= 66 | github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc h1:R83G5ikgLMxrBvLh22JhdfI8K6YXEPHx5P03Uu3DRs4= 67 | github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= 68 | github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 69 | golang.org/x/crypto v0.0.0-20180816225734-aabede6cba87 h1:gCHhzI+1R9peHIMyiWVxoVaWlk1cYK7VThX5ptLtbXY= 70 | golang.org/x/crypto v0.0.0-20180816225734-aabede6cba87/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 71 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= 72 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 73 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 74 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= 75 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 76 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 77 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 78 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM= 79 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 80 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= 81 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 82 | google.golang.org/grpc v1.14.0 h1:ArxJuB1NWfPY6r9Gp9gqwplT0Ge7nqv9msgu03lHLmo= 83 | google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= 84 | gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 86 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 87 | gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= 88 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 89 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 90 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 91 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 92 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 93 | k8s.io/api v0.0.0-20180817212534-2315b41a07e8 h1:SpU65DBNMdCenIikZusc83/mAF7vQFuVp4svBtHAPfw= 94 | k8s.io/api v0.0.0-20180817212534-2315b41a07e8/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA= 95 | k8s.io/apimachinery v0.0.0-20180228050457-302974c03f7e h1:CsgbEA8905OlpVLNKWD4GacPex50kFbqhotVNPew+dU= 96 | k8s.io/apimachinery v0.0.0-20180228050457-302974c03f7e/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0= 97 | k8s.io/client-go v7.0.0+incompatible h1:kiH+Y6hn+pc78QS/mtBfMJAMIIaWevHi++JvOGEEQp4= 98 | k8s.io/client-go v7.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s= 99 | -------------------------------------------------------------------------------- /internal/app/anchor.go: -------------------------------------------------------------------------------- 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 app 9 | 10 | import ( 11 | "strings" 12 | 13 | "github.com/containernetworking/cni/pkg/skel" 14 | "github.com/containernetworking/cni/pkg/types" 15 | "github.com/containernetworking/cni/pkg/types/current" 16 | 17 | "github.com/coreos/etcd/pkg/transport" 18 | 19 | "github.com/hainesc/anchor/internal/pkg/config" 20 | "github.com/hainesc/anchor/pkg/allocator/anchor" 21 | "github.com/hainesc/anchor/pkg/runtime/k8s" 22 | "github.com/hainesc/anchor/pkg/store/etcd" 23 | ) 24 | 25 | // CmdAdd allocates IP for pod 26 | func CmdAdd(args *skel.CmdArgs) error { 27 | ipamConf, confVersion, err := config.LoadIPAMConf(args.StdinData, args.Args) 28 | if err != nil { // Error in config file. 29 | return err 30 | } 31 | 32 | alloc, err := newAllocator(args, ipamConf) 33 | if err != nil { // Error during init Allocator 34 | return err 35 | } 36 | 37 | // Init result here, which will be printed in json format. 38 | result := ¤t.Result{} 39 | 40 | // Handle customized network configurations. 41 | if result, err = alloc.CustomizeGateway(result); err != nil { 42 | return err 43 | } 44 | if result, err = alloc.CustomizeRoutes(result); err != nil { 45 | return err 46 | } 47 | if result, err = alloc.CustomizeDNS(result); err != nil { 48 | return err 49 | } 50 | // Add an item of route: 51 | // service-cluster-ip-range -> Node IP 52 | if result, err = alloc.AddServiceRoute(result, ipamConf.ServiceIPNet, ipamConf.NodeIPs); err != nil { 53 | return err 54 | } 55 | 56 | ipConf, err := alloc.Allocate(args.ContainerID) 57 | if err != nil { 58 | return err 59 | } 60 | result.IPs = append(result.IPs, ipConf) 61 | return types.PrintResult(result, confVersion) 62 | } 63 | 64 | // CmdDel deletes IP for pod 65 | func CmdDel(args *skel.CmdArgs) error { 66 | ipamConf, _, err := config.LoadIPAMConf(args.StdinData, args.Args) 67 | if err != nil { // Error in config file. 68 | return err 69 | } 70 | 71 | cleaner, err := newCleaner(args, ipamConf) 72 | return cleaner.Clean(args.ContainerID) 73 | } 74 | 75 | func newAllocator(args *skel.CmdArgs, conf *config.IPAMConf) (*anchor.Allocator, error) { 76 | tlsInfo := &transport.TLSInfo{ 77 | CertFile: conf.CertFile, 78 | KeyFile: conf.KeyFile, 79 | TrustedCAFile: conf.TrustedCAFile, 80 | } 81 | tlsConfig, _ := tlsInfo.ClientConfig() 82 | // Use etcd as store 83 | store, err := etcd.NewEtcdClient(conf.Name, 84 | strings.Split(conf.Endpoints, ","), 85 | tlsConfig) 86 | defer store.Close() 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | // 1. Get K8S_POD_NAME and K8S_POD_NAMESPACE. 92 | k8sArgs := k8s.Args{} 93 | if err := types.LoadArgs(args.Args, &k8sArgs); err != nil { 94 | return nil, err 95 | } 96 | 97 | // 2. Get conf for k8s client and create a k8s_client 98 | runtime, err := k8s.NewK8sClient(conf.Kubernetes, conf.Policy) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | // 3. Get annotations from k8s_client via K8S_POD_NAME and K8S_POD_NAMESPACE. 104 | label, annot, err := k8s.GetK8sPodInfo(runtime, string(k8sArgs.K8S_POD_NAME), string(k8sArgs.K8S_POD_NAMESPACE)) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | customized := make(map[string]string) 110 | for k, v := range label { 111 | customized[k] = v 112 | } 113 | for k, v := range annot { 114 | customized[k] = v 115 | } 116 | 117 | // It is friendly to show which controller the pods controled by. 118 | // TODO: maybe it is meaningless. the pod name starts with the controller name. 119 | controllerName, _ := k8s.ResourceControllerName(runtime, string(k8sArgs.K8S_POD_NAME), string(k8sArgs.K8S_POD_NAMESPACE)) 120 | if controllerName != "" { 121 | customized["cni.anchor.org/controller"] = controllerName 122 | } 123 | 124 | return anchor.NewAllocator(store, string(k8sArgs.K8S_POD_NAME), 125 | string(k8sArgs.K8S_POD_NAMESPACE), customized) 126 | } 127 | 128 | func newCleaner(args *skel.CmdArgs, ipamConf *config.IPAMConf) (*anchor.Cleaner, error) { 129 | tlsInfo := &transport.TLSInfo{ 130 | CertFile: ipamConf.CertFile, 131 | KeyFile: ipamConf.KeyFile, 132 | TrustedCAFile: ipamConf.TrustedCAFile, 133 | } 134 | tlsConfig, _ := tlsInfo.ClientConfig() 135 | 136 | store, err := etcd.NewEtcdClient(ipamConf.Name, 137 | strings.Split(ipamConf.Endpoints, ","), tlsConfig) 138 | defer store.Close() 139 | if err != nil { 140 | return nil, err 141 | } 142 | 143 | // Read pod name and namespace from args 144 | k8sArgs := k8s.Args{} 145 | if err := types.LoadArgs(args.Args, &k8sArgs); err != nil { 146 | return nil, err 147 | } 148 | 149 | return anchor.NewCleaner(store, 150 | string(k8sArgs.K8S_POD_NAME), 151 | string(k8sArgs.K8S_POD_NAMESPACE)) 152 | } 153 | -------------------------------------------------------------------------------- /internal/app/octopus.go: -------------------------------------------------------------------------------- 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 app 9 | 10 | import ( 11 | "fmt" 12 | "github.com/vishvananda/netlink" 13 | ) 14 | 15 | // ModeFromString configures different mode for macvlan 16 | func ModeFromString(s string) (netlink.MacvlanMode, error) { 17 | switch s { 18 | case "", "bridge": 19 | return netlink.MACVLAN_MODE_BRIDGE, nil 20 | case "private": 21 | return netlink.MACVLAN_MODE_PRIVATE, nil 22 | case "vepa": 23 | return netlink.MACVLAN_MODE_VEPA, nil 24 | case "passthru": 25 | return netlink.MACVLAN_MODE_PASSTHRU, nil 26 | default: 27 | return 0, fmt.Errorf("unknown macvlan mode: %q", s) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /internal/pkg/config/config.go: -------------------------------------------------------------------------------- 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 config 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | 14 | "github.com/containernetworking/cni/pkg/types" 15 | "github.com/hainesc/anchor/pkg/runtime/k8s" 16 | ) 17 | 18 | // OctopusConf represents the Octopus configuration. 19 | type OctopusConf struct { 20 | types.NetConf 21 | Mode string `json:"mode"` 22 | MTU int `json:"mtu"` 23 | Octopus map[string]string `json:"octopus"` 24 | Kubernetes k8s.Kubernetes `json:"kubernetes"` 25 | Policy k8s.Policy `json:"policy"` 26 | } 27 | 28 | // IPAMConf represents the IPAM configuration. 29 | type IPAMConf struct { 30 | Name string 31 | Type string `json:"type"` 32 | // etcd client 33 | Endpoints string `json:"etcd_endpoints"` 34 | // Used for k8s client 35 | Kubernetes k8s.Kubernetes `json:"kubernetes"` 36 | Policy k8s.Policy `json:"policy"` 37 | // etcd perm files 38 | CertFile string `json:"etcd_cert_file"` 39 | KeyFile string `json:"etcd_key_file"` 40 | TrustedCAFile string `json:"etcd_ca_cert_file"` 41 | ServiceIPNet string `json:"service_ipnet"` 42 | NodeIPs []string `json:"node_ips"` 43 | // Additional network config for pods 44 | Routes []*types.Route `json:"routes,omitempty"` 45 | ResolvConf string `json:"resolvConf,omitempty"` 46 | } 47 | 48 | // CNIConf represents the top-level network config. 49 | type CNIConf struct { 50 | Name string `json:"name"` 51 | CNIVersion string `json:"cniVersion"` 52 | Type string `json:"type"` 53 | Master string `json:"master"` 54 | IPAM *IPAMConf `json:"ipam"` 55 | } 56 | 57 | // LoadOctopusConf loads config from bytes which read from config file for octopus. 58 | func LoadOctopusConf(bytes []byte) (*OctopusConf, string, error) { 59 | n := &OctopusConf{} 60 | if err := json.Unmarshal(bytes, n); err != nil { 61 | return nil, "", fmt.Errorf("failed to load netconf: %v", err) 62 | } 63 | 64 | if n.Octopus == nil { 65 | return nil, "", fmt.Errorf(`"octopus" field is required. It specifies a list of interface names to virtualize`) 66 | } 67 | 68 | return n, n.CNIVersion, nil 69 | } 70 | 71 | // LoadIPAMConf loads config from bytes which read from config file for anchor. 72 | func LoadIPAMConf(bytes []byte, envArgs string) (*IPAMConf, string, error) { 73 | n := CNIConf{} 74 | if err := json.Unmarshal(bytes, &n); err != nil { 75 | return nil, "", err 76 | } 77 | 78 | if n.IPAM == nil { 79 | return nil, "", fmt.Errorf("IPAM config missing 'ipam' key") 80 | } 81 | 82 | if n.IPAM.Endpoints == "" { 83 | return nil, "", fmt.Errorf("IPAM config missing 'etcd_endpoints' keys") 84 | } 85 | return n.IPAM, n.CNIVersion, nil 86 | } 87 | -------------------------------------------------------------------------------- /internal/pkg/customized/customized.go: -------------------------------------------------------------------------------- 1 | package customized 2 | -------------------------------------------------------------------------------- /internal/pkg/customized/doc.go: -------------------------------------------------------------------------------- 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 | /* 9 | User defined config 10 | 11 | User defined gateway 12 | User defined range 13 | User defined routes 14 | User defined DNS 15 | */ 16 | 17 | package customized 18 | -------------------------------------------------------------------------------- /pkg/allocator/allocator.go: -------------------------------------------------------------------------------- 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 allocator 9 | 10 | import ( 11 | "github.com/containernetworking/cni/pkg/types/current" 12 | ) 13 | 14 | // Allocator is the interface for allocator 15 | type Allocator interface { 16 | CustomizeGateway(ret *current.Result) (*current.Result, error) 17 | CustomizeRoutes(ret *current.Result) (*current.Result, error) 18 | CustomizeDNS(ret *current.Result) (*current.Result, error) 19 | AddServiceRoute(ret *current.Result, serviceClusterIPRange string, nodeIPs []string) (*current.Result, error) 20 | 21 | Allocate(id string) (*current.IPConfig, error) 22 | } 23 | 24 | // Cleaner is the interface for cleaner 25 | type Cleaner interface { 26 | Clean(id string) error 27 | } 28 | -------------------------------------------------------------------------------- /pkg/allocator/anchor/anchor.go: -------------------------------------------------------------------------------- 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 anchor 9 | 10 | import ( 11 | "fmt" 12 | "github.com/containernetworking/cni/pkg/types" 13 | "github.com/containernetworking/cni/pkg/types/current" 14 | "github.com/containernetworking/plugins/pkg/ip" 15 | "github.com/hainesc/anchor/pkg/allocator" 16 | "github.com/hainesc/anchor/pkg/store" 17 | "net" 18 | "strings" 19 | ) 20 | 21 | // Allocator is the allocator for anchor. 22 | type Allocator struct { 23 | store store.Store 24 | pod string 25 | namespace string 26 | customized map[string]string 27 | subnet *net.IPNet 28 | gateway net.IP 29 | } 30 | 31 | const ( 32 | customizeGatewayKey = "cni.anchor.org/gateway" 33 | customizeRoutesKey = "cni.anchor.org/routes" 34 | customizeSubnetKey = "cni.anchor.org/subnet" 35 | customizeRangeKey = "cni.anchor.org/range" 36 | ) 37 | 38 | // AnchorAllocator implements the Allocator interface 39 | var _ allocator.Allocator = &Allocator{} 40 | 41 | // NewAllocator news a allocator 42 | func NewAllocator(store store.Store, 43 | pod, namespace string, 44 | customized map[string]string) (*Allocator, error) { 45 | var subnet *net.IPNet 46 | var gw net.IP 47 | var err error // declaration here for avoid := which also create a new var 48 | if customized[customizeSubnetKey] != "" { 49 | _, subnet, err = net.ParseCIDR(customized[customizeSubnetKey]) 50 | if err != nil { 51 | return nil, fmt.Errorf("invalid format of subnet in annotations") 52 | } 53 | } 54 | if customized[customizeRangeKey] != "" { 55 | // TODO: Caculate the subnet 56 | return nil, fmt.Errorf("customized range not implentmented") 57 | } 58 | 59 | if customized[customizeGatewayKey] != "" { 60 | gw = net.ParseIP(customized[customizeSubnetKey]) 61 | if gw == nil { 62 | return nil, fmt.Errorf("invalid format of gateway in annotations") 63 | } 64 | } else { 65 | // Maybe no lock here is better. 66 | store.Lock() 67 | defer store.Unlock() 68 | gw = store.RetrieveGateway(subnet) 69 | if gw == nil { 70 | return nil, fmt.Errorf("failed to retrieve gateway for %s", subnet.String()) 71 | } 72 | } 73 | 74 | if !subnet.Contains(gw) { 75 | return nil, fmt.Errorf("gateway %s not in network %s", gw.String(), subnet.String()) 76 | } 77 | return &Allocator{ 78 | store: store, 79 | pod: pod, 80 | namespace: namespace, 81 | customized: customized, 82 | subnet: subnet, 83 | gateway: gw, 84 | }, nil 85 | } 86 | 87 | // CustomizeGateway adds default route for pod if customizeGatewayKey is set. 88 | func (a *Allocator) CustomizeGateway(ret *current.Result) (*current.Result, error) { 89 | // We do nothing here because we has set gateway in func NewAllocator. 90 | /* 91 | if customizeGateway := net.ParseIP(a.customized[customizeGatewayKey]); 92 | customizeGateway != nil { 93 | ret.Routes = append(ret.Routes, &types.Route{ 94 | Dst: net.IPNet{ 95 | IP: net.IPv4zero, 96 | Mask: net.IPv4Mask(0, 0, 0, 0), 97 | }, 98 | GW: customizeGateway, 99 | }) 100 | a.gatewayCustomized = true 101 | } 102 | */ 103 | return ret, nil 104 | } 105 | 106 | // CustomizeRoutes adds routes if customizeRouteKey is set. 107 | // The format of input should as: 10.0.1.0/24,10.0.1.1;10.0.5.0/24,10.0.5.1 108 | // The outer delimiter is semicolon(;) and the inner delimiter is comma(,) 109 | func (a *Allocator) CustomizeRoutes(ret *current.Result) (*current.Result, error) { 110 | // Config route for default first 111 | ret.Routes = append(ret.Routes, &types.Route{ 112 | Dst: net.IPNet{ 113 | IP: net.IPv4zero, 114 | Mask: net.IPv4Mask(0, 0, 0, 0), 115 | }, 116 | GW: a.gateway, 117 | }) 118 | 119 | if customizeRoute := a.customized[customizeRoutesKey]; customizeRoute != "" { 120 | routes := strings.Split(customizeRoute, ";") 121 | for _, r := range routes { 122 | _, dst, err := net.ParseCIDR(strings.Split(r, ",")[0]) 123 | if err != nil { 124 | return nil, fmt.Errorf("invalid format of customized route in %s", r) 125 | } 126 | gw := net.ParseIP(strings.Split(r, ",")[1]) 127 | if gw == nil { 128 | return nil, fmt.Errorf("invalid format of customized route in %s", r) 129 | } 130 | 131 | if !a.subnet.Contains(gw) { 132 | return nil, fmt.Errorf("gateway %s not in network %s", gw.String(), a.subnet.String()) 133 | } 134 | ret.Routes = append(ret.Routes, &types.Route{ 135 | Dst: *dst, 136 | GW: gw, 137 | }) 138 | } 139 | } 140 | 141 | return ret, nil 142 | } 143 | 144 | // CustomizeDNS configs the DNS for pod, but not implemented now. 145 | func (a *Allocator) CustomizeDNS(ret *current.Result) (*current.Result, error) { 146 | // Recently, k8s does nothing even our result contains DNS info. 147 | // So we do nothing, just a function interface here. 148 | return ret, nil 149 | } 150 | 151 | // AddServiceRoute adds route for serice cluster ip range. 152 | func (a *Allocator) AddServiceRoute(ret *current.Result, 153 | serviceClusterIPRange string, 154 | nodeIPs []string) (*current.Result, error) { 155 | if serviceClusterIPRange == "" { 156 | return ret, nil 157 | } 158 | _, dst, err := net.ParseCIDR(serviceClusterIPRange) 159 | if err != nil { 160 | return nil, fmt.Errorf("invalid format of service cluster ip range %s", serviceClusterIPRange) 161 | } 162 | 163 | for _, nodeIP := range nodeIPs { 164 | if a.subnet.Contains(net.ParseIP(nodeIP)) { 165 | ret.Routes = append(ret.Routes, &types.Route{ 166 | Dst: *dst, 167 | GW: net.ParseIP(nodeIP), 168 | }) 169 | break 170 | } 171 | // If none of nodeIP contains in subnet, just break. 172 | } 173 | return ret, nil 174 | } 175 | 176 | // Allocate allocates IP for the pod. 177 | func (a *Allocator) Allocate(id string) (*current.IPConfig, error) { 178 | a.store.Lock() 179 | defer a.store.Unlock() 180 | ips, err := a.store.RetrieveAllocated(a.namespace, a.subnet) 181 | if err != nil { 182 | return nil, err 183 | } 184 | for _, ipRange := range *ips { 185 | ipRange.Gateway = a.gateway 186 | if err = ipRange.Canonicalize(); err != nil { 187 | return nil, err 188 | } 189 | } 190 | used, err := a.store.RetrieveUsed(a.namespace, a.subnet) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | for _, usedRange := range *used { 196 | usedRange.Gateway = a.gateway 197 | if err = usedRange.Canonicalize(); err != nil { 198 | return nil, err 199 | } 200 | } 201 | for _, r := range *ips { 202 | var iter net.IP 203 | for iter = r.RangeStart; !iter.Equal(ip.NextIP(r.RangeEnd)); iter = ip.NextIP(iter) { 204 | 205 | avail := true 206 | for _, usedRange := range *used { 207 | var u net.IP 208 | for u = usedRange.RangeStart; !u.Equal(ip.NextIP(usedRange.RangeEnd)); u = ip.NextIP(u) { 209 | if iter.Equal(u) { 210 | avail = false 211 | break 212 | } 213 | } 214 | } 215 | if avail { 216 | // Get subnet and gateway information 217 | if err != nil { 218 | // TODO: check 219 | continue 220 | } 221 | if iter.Equal(a.gateway) { 222 | // TODO: check 223 | continue 224 | } 225 | // TODO: 226 | 227 | controllerName := a.customized["cni.anchor.org/controller"] 228 | if controllerName == "" { 229 | controllerName = "unknown" 230 | } 231 | _, err = a.store.Reserve(id, iter, a.pod, a.namespace, controllerName) 232 | if err != nil { 233 | continue 234 | } 235 | 236 | return ¤t.IPConfig{ 237 | Version: "4", 238 | Address: net.IPNet{IP: iter, Mask: a.subnet.Mask}, 239 | Gateway: a.gateway, 240 | }, nil 241 | } 242 | } 243 | } 244 | return nil, fmt.Errorf("can not allcate IP for pod named, %s", a.pod) 245 | } 246 | 247 | // Cleaner is the cleaner for anchor. 248 | type Cleaner struct { 249 | store store.Store 250 | pod string 251 | namespace string 252 | } 253 | 254 | // AnchorCleaner implements the Cleaner interface 255 | var _ allocator.Cleaner = &Cleaner{} 256 | 257 | // NewCleaner news a cleaner for anchor. 258 | func NewCleaner(store store.Store, pod, namespace string) (*Cleaner, error) { 259 | return &Cleaner{ 260 | store: store, 261 | pod: pod, 262 | namespace: namespace, 263 | }, nil 264 | } 265 | 266 | // Clean cleans the IP for the pod. 267 | func (a *Cleaner) Clean(id string) error { 268 | a.store.Lock() 269 | defer a.store.Unlock() 270 | return a.store.Release(id) 271 | } 272 | -------------------------------------------------------------------------------- /pkg/monkey/config.go: -------------------------------------------------------------------------------- 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 monkey 9 | 10 | import ( 11 | "encoding/json" 12 | "fmt" 13 | "io/ioutil" 14 | "os" 15 | "path/filepath" 16 | "sort" 17 | ) 18 | 19 | // CaptainConf is the config for Caption, Maybe we should rename it to MonkeyConf. 20 | type CaptainConf struct { 21 | // etcd client 22 | Endpoints string `json:"etcd_endpoints"` 23 | // etcd perm files 24 | CertFile string `json:"etcd_cert_file"` 25 | KeyFile string `json:"etcd_key_file"` 26 | TrustedCAFile string `json:"etcd_ca_cert_file"` 27 | } 28 | 29 | // NotFoundError when the directory and file not found. 30 | type NotFoundError struct { 31 | Dir string 32 | Name string 33 | } 34 | 35 | // Error prints out the error message for NotFoundError 36 | func (e NotFoundError) Error() string { 37 | return fmt.Sprintf(`no net configuration with name "%s" in %s`, e.Name, e.Dir) 38 | } 39 | 40 | // NoConfigsFoundError when no config files found in dir 41 | type NoConfigsFoundError struct { 42 | Dir string 43 | } 44 | 45 | // Error print out the error message for NoConfigFoundError 46 | func (e NoConfigsFoundError) Error() string { 47 | return fmt.Sprintf(`no net configurations found in %s`, e.Dir) 48 | } 49 | 50 | // LoadConf loads config for Monkey 51 | func LoadConf(dir, name string) (*CaptainConf, error) { 52 | files, err := ConfFiles(dir, []string{".conf", ".json"}) 53 | switch { 54 | case err != nil: 55 | return nil, err 56 | case len(files) == 0: 57 | return nil, NoConfigsFoundError{Dir: dir} 58 | } 59 | sort.Strings(files) 60 | 61 | for _, confFile := range files { 62 | conf, err := ConfFromFile(confFile) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return conf, nil 67 | 68 | } 69 | return nil, NotFoundError{dir, name} 70 | 71 | } 72 | 73 | // ConfFromFile reads config from file 74 | func ConfFromFile(filename string) (*CaptainConf, error) { 75 | bytes, err := ioutil.ReadFile(filename) 76 | if err != nil { 77 | return nil, fmt.Errorf("error reading %s: %s", filename, err) 78 | } 79 | return ConfFromBytes(bytes) 80 | } 81 | 82 | // ConfFromBytes reads config from byte array 83 | func ConfFromBytes(bytes []byte) (*CaptainConf, error) { 84 | conf := &CaptainConf{} 85 | if err := json.Unmarshal(bytes, &conf); err != nil { 86 | return nil, fmt.Errorf("error parsing configuration: %s", err) 87 | } 88 | return conf, nil 89 | } 90 | 91 | // ConfFiles returns config file list in the dir which has specail extensions 92 | func ConfFiles(dir string, extensions []string) ([]string, error) { 93 | files, err := ioutil.ReadDir(dir) 94 | switch { 95 | case err == nil: // break 96 | case os.IsNotExist(err): 97 | return nil, nil 98 | default: 99 | return nil, err 100 | } 101 | 102 | confFiles := []string{} 103 | for _, f := range files { 104 | if f.IsDir() { 105 | continue 106 | } 107 | fileExt := filepath.Ext(f.Name()) 108 | for _, ext := range extensions { 109 | if fileExt == ext { 110 | confFiles = append(confFiles, filepath.Join(dir, f.Name())) 111 | } 112 | } 113 | } 114 | return confFiles, nil 115 | } 116 | -------------------------------------------------------------------------------- /pkg/monkey/handler.go: -------------------------------------------------------------------------------- 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 monkey 9 | 10 | import ( 11 | "encoding/json" 12 | "github.com/hainesc/anchor/pkg/store/etcd" 13 | "log" 14 | "net/http" 15 | "strings" 16 | ) 17 | 18 | // InUseHandler handlers the get request from front end and returns IPs in use. 19 | // TODO: It should has a member named user_namespace which can filter the result. 20 | type InUseHandler struct { 21 | etcd *etcd.Etcd 22 | } 23 | 24 | // NewInUseHandler news an InUsedHandler 25 | func NewInUseHandler(etcd *etcd.Etcd) *InUseHandler { 26 | return &InUseHandler{ 27 | etcd: etcd, 28 | } 29 | } 30 | 31 | // GatewayHandler handles the request for gateway information 32 | type GatewayHandler struct { 33 | etcd *etcd.Etcd 34 | } 35 | 36 | // NewGatewayHandler news a GatewayHandler 37 | func NewGatewayHandler(etcd *etcd.Etcd) *GatewayHandler { 38 | return &GatewayHandler{ 39 | etcd: etcd, 40 | } 41 | } 42 | 43 | // AllocateHandler handles the request for allocate information 44 | type AllocateHandler struct { 45 | etcd *etcd.Etcd 46 | } 47 | 48 | // NewAllocateHandler news a Allocator handler 49 | func NewAllocateHandler(etcd *etcd.Etcd) *AllocateHandler { 50 | return &AllocateHandler{ 51 | etcd: etcd, 52 | } 53 | } 54 | 55 | // ServeHTTP serves http 56 | func (h *GatewayHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 57 | switch r.Method { 58 | case http.MethodGet: 59 | // Serve the resource. 60 | // TODO: just for test. 61 | log.Printf("receive a get mothod with parameter: ") 62 | gws, _ := h.etcd.AllGatewayMap() 63 | response, _ := json.Marshal(gws) 64 | // TODO: if error. 65 | w.Write(response) 66 | case http.MethodPost: 67 | // Create a new record. 68 | // curl -X POST -d "{\"subnet\": \"10.2.1.0/24\", \"gw\": \"10.2.1.1\"}" http://localhost:3000/api/v1/gateway 69 | log.Printf("receive a post mothod with parameter: ") 70 | // gms := make([]etcd.GatewayMap, 0) 71 | var gm etcd.GatewayMap 72 | decoder := json.NewDecoder(r.Body) 73 | err := decoder.Decode(&gm) 74 | if err != nil { 75 | // TODO: 76 | http.Error(w, "Invalid parameter.", 405) 77 | } 78 | 79 | // TODO: valid the input. 80 | // TODO: check if exists. 81 | log.Printf("%s: %s", gm.Subnet, gm.Gateway) 82 | 83 | err = h.etcd.InsertGatewayMap(gm) 84 | if err != nil { 85 | log.Printf(err.Error()) 86 | } 87 | 88 | case http.MethodPut: 89 | // Update an existing record. 90 | log.Printf("receive a post mothod with parameter: ") 91 | case http.MethodDelete: 92 | // Remove the record. 93 | // curl -X DELETE -d "[{\"subnet\": \"10.2.1.0/24\", \"gw\": \"10.2.1.1\"}]" http://localhost:3000/api/v1/gateway 94 | log.Printf("receive a delete mothod with parameter: ") 95 | gms := make([]etcd.GatewayMap, 0) 96 | // var gms etcd.GatewayMap 97 | decoder := json.NewDecoder(r.Body) 98 | err := decoder.Decode(&gms) 99 | if err != nil { 100 | // TODO: 101 | http.Error(w, "Invalid parameter.", 405) 102 | } 103 | for _, gm := range gms { 104 | log.Printf("%s: %s", gm.Subnet, gm.Gateway) 105 | 106 | } 107 | err = h.etcd.DeleteGatewayMap(gms) 108 | if err != nil { 109 | log.Printf(err.Error()) 110 | } 111 | // TODO: recently, angular delete method does not support body parameter, but it is in process. So we just use patch here. see: https://github.com/angular/angular/issues/19438 112 | // Remove this case when the support is done. 113 | case http.MethodPatch: 114 | // Remove the record. 115 | // curl -X DELETE -d "[{\"subnet\": \"10.2.1.0/24\", \"gw\": \"10.2.1.1\"}]" http://localhost:3000/api/v1/gateway 116 | log.Printf("receive a delete mothod with parameter: ") 117 | gms := make([]etcd.GatewayMap, 0) 118 | // var gms etcd.GatewayMap 119 | decoder := json.NewDecoder(r.Body) 120 | err := decoder.Decode(&gms) 121 | if err != nil { 122 | // TODO: 123 | http.Error(w, "Invalid parameter.", 405) 124 | } 125 | for _, gm := range gms { 126 | log.Printf("%s: %s", gm.Subnet, gm.Gateway) 127 | 128 | } 129 | err = h.etcd.DeleteGatewayMap(gms) 130 | if err != nil { 131 | log.Printf(err.Error()) 132 | } 133 | default: 134 | // Give an error message. 135 | http.Error(w, "Invalid request method.", 405) 136 | } 137 | } 138 | 139 | // ServeHTTP serves http 140 | func (h *AllocateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 141 | switch r.Method { 142 | case http.MethodGet: 143 | // Serve the resource. 144 | // TODO: just for test. 145 | log.Printf("receive a get mothod with parameter: ") 146 | ams, _ := h.etcd.AllAllocate() 147 | response, _ := json.Marshal(ams) 148 | // TODO: if error. 149 | w.Write(response) 150 | case http.MethodPost: 151 | // Create a new record. 152 | log.Printf("receive a post mothod with parameter: ") 153 | // gms := make([]etcd.GatewayMap, 0) 154 | var am etcd.AllocateMap 155 | decoder := json.NewDecoder(r.Body) 156 | err := decoder.Decode(&am) 157 | if err != nil { 158 | // TODO: 159 | http.Error(w, "Invalid parameter.", 405) 160 | } 161 | 162 | // TODO: valid the input. 163 | // TODO: check if exists. 164 | log.Printf("%s: %s", am.Namespace, am.Allocate) 165 | 166 | err = h.etcd.InsertAllocateMap(am) 167 | if err != nil { 168 | log.Printf(err.Error()) 169 | } 170 | 171 | case http.MethodPut: 172 | // Update an existing record. 173 | log.Printf("receive a post mothod with parameter: ") 174 | case http.MethodDelete: 175 | // Remove the record. 176 | log.Printf("receive a delete mothod with parameter: ") 177 | ams := make([]etcd.AllocateMap, 0) 178 | // var gms etcd.GatewayMap 179 | decoder := json.NewDecoder(r.Body) 180 | err := decoder.Decode(&ams) 181 | if err != nil { 182 | // TODO: 183 | http.Error(w, "Invalid parameter.", 505) 184 | } 185 | for _, am := range ams { 186 | log.Printf("%s: %s", am.Namespace, am.Allocate) 187 | 188 | } 189 | err = h.etcd.DeleteAllocateMap(ams) 190 | if err != nil { 191 | log.Printf(err.Error()) 192 | } 193 | // TODO: recently, angular delete method does not support body parameter, but it is in process. So we just use patch here. see: https://github.com/angular/angular/issues/19438 194 | // Remove this case when the support is done. 195 | case http.MethodPatch: 196 | // Remove the record. 197 | log.Printf("receive a delete mothod with parameter: ") 198 | ams := make([]etcd.AllocateMap, 0) 199 | // var gms etcd.GatewayMap 200 | decoder := json.NewDecoder(r.Body) 201 | err := decoder.Decode(&ams) 202 | if err != nil { 203 | // TODO: 204 | http.Error(w, "Invalid parameter.", 505) 205 | } 206 | for _, am := range ams { 207 | log.Printf("%s: %s", am.Namespace, am.Allocate) 208 | 209 | } 210 | err = h.etcd.DeleteAllocateMap(ams) 211 | if err != nil { 212 | log.Printf(err.Error()) 213 | } 214 | default: 215 | // Give an error message. 216 | http.Error(w, "Invalid request method.", 405) 217 | } 218 | } 219 | 220 | // TODO: 221 | type ips struct { 222 | IP string `json:"ip"` 223 | Pod string `json:"pod"` 224 | Namespace string `json:"ns"` 225 | Controller string `json:"ctrl"` 226 | // App string `json:"app"` 227 | // Service string `json:"svc"` 228 | } 229 | 230 | // TODO: 231 | type gateway struct { 232 | Subnet string `json:"subnet"` 233 | Dst string `json:"dst"` 234 | } 235 | 236 | // ServeHTTP serves http 237 | func (h *InUseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 238 | empty := []byte{} 239 | w.Header().Set("Content-Type", "application/json") 240 | // namespace := r.Header.Get("x-dce-tenant") 241 | namespace := "default" 242 | if namespace == "" { 243 | w.Write(empty) 244 | return 245 | } 246 | 247 | result := []ips{} 248 | // TODO: check header and find whether is admin role. 249 | ipsInArray, err := h.etcd.RetrieveUsedbyNamespace("default", true) 250 | if err != nil { 251 | // TODO: if err, we return an empty here. 252 | w.Write(empty) 253 | } 254 | 255 | for _, r := range *ipsInArray { 256 | parts := strings.Split(r, ",") 257 | result = append(result, ips{ 258 | IP: parts[0], 259 | Pod: parts[1], 260 | Namespace: parts[2], 261 | Controller: parts[4], 262 | }) 263 | } 264 | resultInJSON, err := json.Marshal(result) 265 | if err != nil { 266 | log.Printf("empty, ", err.Error()) 267 | w.Write(empty) 268 | } 269 | log.Printf("empty, ", string(resultInJSON)) 270 | w.Write(resultInJSON) 271 | } 272 | -------------------------------------------------------------------------------- /pkg/runtime/k8s/k8s.go: -------------------------------------------------------------------------------- 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 k8s 9 | 10 | import ( 11 | "fmt" 12 | "strings" 13 | 14 | "k8s.io/apimachinery/pkg/apis/meta/v1" 15 | "k8s.io/client-go/kubernetes" 16 | "k8s.io/client-go/tools/clientcmd" 17 | ) 18 | 19 | // NewK8sClient creates a k8s client 20 | func NewK8sClient(kuber Kubernetes, policy Policy) (*kubernetes.Clientset, error) { 21 | // Some config can be passed in a kubeconfig file 22 | kubeconfig := kuber.Kubeconfig 23 | // Config can be overridden by config passed in explicitly in the network config. 24 | configOverrides := &clientcmd.ConfigOverrides{} 25 | 26 | // If an API root is given, make sure we're using using the name / port rather than 27 | // the full URL. Earlier versions of the config required the full `/api/v1/` extension, 28 | // so split that off to ensure compatibility. 29 | policy.K8sAPIRoot = strings.Split(policy.K8sAPIRoot, "/api/")[0] 30 | 31 | var overridesMap = []struct { 32 | variable *string 33 | value string 34 | }{ 35 | {&configOverrides.ClusterInfo.Server, policy.K8sAPIRoot}, 36 | {&configOverrides.AuthInfo.ClientCertificate, policy.K8sClientCertificate}, 37 | {&configOverrides.AuthInfo.ClientKey, policy.K8sClientKey}, 38 | {&configOverrides.ClusterInfo.CertificateAuthority, policy.K8sCertificateAuthority}, 39 | {&configOverrides.AuthInfo.Token, policy.K8sAuthToken}, 40 | } 41 | 42 | // Using the override map above, populate any non-empty values. 43 | for _, override := range overridesMap { 44 | if override.value != "" { 45 | *override.variable = override.value 46 | } 47 | } 48 | 49 | // Also allow the K8sAPIRoot to appear under the "kubernetes" block in the network config. 50 | if kuber.K8sAPIRoot != "" { 51 | configOverrides.ClusterInfo.Server = kuber.K8sAPIRoot 52 | } 53 | 54 | // Use the kubernetes client code to load the kubeconfig file and combine it with the overrides. 55 | config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 56 | &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}, 57 | configOverrides).ClientConfig() 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | // Create the clientset 63 | return kubernetes.NewForConfig(config) 64 | } 65 | 66 | // GetK8sPodInfo gets the labels and annotations of the pod 67 | func GetK8sPodInfo(client *kubernetes.Clientset, podName, podNamespace string) (labels map[string]string, annotations map[string]string, err error) { 68 | pod, err := client.CoreV1().Pods(string(podNamespace)).Get(podName, v1.GetOptions{}) 69 | if err != nil { 70 | return nil, nil, err 71 | } 72 | return pod.Labels, pod.Annotations, nil 73 | } 74 | 75 | // ResourceControllerName gets the name of ResourceController based on given reference. 76 | func ResourceControllerName(client *kubernetes.Clientset, podName, namespace string) ( 77 | string, error) { 78 | pod, err := client.CoreV1().Pods(string(namespace)).Get(podName, v1.GetOptions{}) 79 | if err != nil { 80 | return "", err 81 | } 82 | 83 | for _, ref := range pod.OwnerReferences { 84 | if *ref.Controller { 85 | if strings.ToLower(ref.Kind) == "replicaset" { 86 | rs, err := client.AppsV1beta2().ReplicaSets(namespace).Get(ref.Name, v1.GetOptions{}) 87 | if err != nil { 88 | return "", err 89 | } 90 | for _, r := range rs.OwnerReferences { 91 | if *r.Controller { 92 | return r.Name, nil 93 | } 94 | } 95 | return ref.Name, nil 96 | } 97 | return ref.Name, nil 98 | } 99 | } 100 | return "", fmt.Errorf("The pod %s has no controller", podName) 101 | } 102 | -------------------------------------------------------------------------------- /pkg/runtime/k8s/types.go: -------------------------------------------------------------------------------- 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 k8s 9 | 10 | import ( 11 | "net" 12 | 13 | "github.com/containernetworking/cni/pkg/types" 14 | ) 15 | 16 | // Policy is a struct to hold policy config (which currently happens to also contain some K8s config) 17 | type Policy struct { 18 | PolicyType string `json:"type"` 19 | K8sAPIRoot string `json:"k8s_api_root"` 20 | K8sAuthToken string `json:"k8s_auth_token"` 21 | K8sClientCertificate string `json:"k8s_client_certificate"` 22 | K8sClientKey string `json:"k8s_client_key"` 23 | K8sCertificateAuthority string `json:"k8s_certificate_authority"` 24 | } 25 | 26 | // Kubernetes is a K8s specific struct to hold config 27 | type Kubernetes struct { 28 | K8sAPIRoot string `json:"k8s_api_root"` 29 | Kubeconfig string `json:"kubeconfig"` 30 | NodeName string `json:"node_name"` 31 | } 32 | 33 | // Args is the valid CNI_ARGS used for Kubernetes 34 | type Args struct { 35 | types.CommonArgs 36 | IP net.IP 37 | K8S_POD_NAME types.UnmarshallableString 38 | K8S_POD_NAMESPACE types.UnmarshallableString 39 | K8S_POD_INFRA_CONTAINER_ID types.UnmarshallableString 40 | } 41 | -------------------------------------------------------------------------------- /pkg/runtime/runtime.go: -------------------------------------------------------------------------------- 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 runtime 9 | 10 | // Runtime is the interface for runtime of the CNI 11 | type Runtime interface { 12 | } 13 | -------------------------------------------------------------------------------- /pkg/store/etcd/etcd.go: -------------------------------------------------------------------------------- 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 etcd 9 | 10 | import ( 11 | "context" 12 | "crypto/tls" 13 | "fmt" 14 | "net" 15 | "strings" 16 | "time" 17 | 18 | "github.com/coreos/etcd/clientv3" 19 | "github.com/coreos/etcd/clientv3/concurrency" 20 | "github.com/hainesc/anchor/pkg/store" 21 | "github.com/hainesc/anchor/pkg/utils" 22 | ) 23 | 24 | const ( 25 | ipsPrefix = "/anchor/cn/" 26 | gatewayPrefix = "/anchor/gw/" 27 | userPrefix = "/anchor/ns/" 28 | lockKey = "/anchor/lock" 29 | ) 30 | 31 | // Etcd is a simple etcd-backed store 32 | type Etcd struct { 33 | mutex *concurrency.Mutex 34 | kv clientv3.KV 35 | } 36 | 37 | // Store implements the Store interface 38 | var _ store.Store = &Etcd{} 39 | 40 | // NewEtcdClient news a etcd client 41 | func NewEtcdClient(network string, endPoints []string, tlsConfig *tls.Config) (*Etcd, error) { 42 | // We don't check the config here since clientv3 will do. 43 | cli, err := clientv3.New(clientv3.Config{ 44 | Endpoints: endPoints, 45 | DialTimeout: 5 * time.Second, 46 | TLS: tlsConfig, 47 | }) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | session, err := concurrency.NewSession(cli) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | mutex := concurrency.NewMutex(session, lockKey) 58 | kv := clientv3.NewKV(cli) 59 | return &Etcd{mutex, kv}, nil 60 | } 61 | 62 | // NewEtcdClientWithoutSSl news a etcd client without ssl 63 | func NewEtcdClientWithoutSSl(network string, endPoints []string) (*Etcd, error) { 64 | // We don't check the config here since clientv3 will do. 65 | cli, err := clientv3.New(clientv3.Config{ 66 | Endpoints: endPoints, 67 | DialTimeout: 5 * time.Second, 68 | }) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | session, err := concurrency.NewSession(cli) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | mutex := concurrency.NewMutex(session, lockKey) 79 | kv := clientv3.NewKV(cli) 80 | return &Etcd{mutex, kv}, nil 81 | } 82 | 83 | // Lock locks the store 84 | func (e *Etcd) Lock() error { 85 | return e.mutex.Lock(context.TODO()) 86 | } 87 | 88 | // Unlock unlocks the store 89 | func (e *Etcd) Unlock() error { 90 | return e.mutex.Unlock(context.TODO()) 91 | } 92 | 93 | // Close closes the store 94 | func (e *Etcd) Close() error { 95 | return nil 96 | // return s.Unlock() 97 | } 98 | 99 | // RetrieveGateway retrieves gateway for subnet. 100 | func (e *Etcd) RetrieveGateway(subnet *net.IPNet) net.IP { 101 | resp, err := e.kv.Get(context.TODO(), gatewayPrefix + subnet.String()) 102 | if err != nil || len(resp.Kvs) == 0 { 103 | return nil 104 | } 105 | return net.ParseIP(string(resp.Kvs[0].Value)) 106 | } 107 | 108 | // RetrieveAllocated retrieves allocated IPs in subnet for namespace. 109 | func (e *Etcd) RetrieveAllocated(namespace string, subnet *net.IPNet) (*utils.RangeSet, error) { 110 | resp, err := e.kv.Get(context.TODO(), userPrefix + namespace) 111 | if err != nil { 112 | return nil, err 113 | } 114 | if len(resp.Kvs) == 0 { 115 | return nil, fmt.Errorf("no IP allocated for %s found in etcd", namespace) 116 | } 117 | // TODO: 118 | ret := utils.RangeSet{} 119 | return ret.Concat(string(resp.Kvs[0].Value), subnet) 120 | 121 | } 122 | 123 | // RetrieveUsed retrieves used IP in subnet for namespace. 124 | func (e *Etcd) RetrieveUsed(namespace string, subnet *net.IPNet) (*utils.RangeSet, error) { 125 | resp, err := e.kv.Get(context.TODO(), ipsPrefix, clientv3.WithPrefix()) 126 | if err != nil { 127 | return nil, err 128 | } 129 | // TODO: which return type is best? []string or RangeSet? 130 | // ret := make([]net.IP, 0) 131 | s := make([]string, 0) 132 | for _, item := range resp.Kvs { 133 | row := strings.Split(string(item.Value), ",") 134 | if row[2] == namespace { 135 | // ret = append(ret, net.ParseIP(row[0])) 136 | s = append(s, row[0]) 137 | } 138 | } 139 | ret := utils.RangeSet{} 140 | // TODO: 141 | return ret.Concat(strings.Join(s, ","), subnet) 142 | } 143 | 144 | // Reserve writes the result to the store. 145 | func (e *Etcd) Reserve(id string, ip net.IP, podName string, podNamespace string, controllerName string) (bool, error) { 146 | // TODO: lock 147 | if _, err := e.kv.Put(context.TODO(), ipsPrefix + id, 148 | ip.String() + "," + podName + "," + podNamespace + "," + controllerName); err != nil { 149 | return false, nil 150 | } 151 | 152 | return true, nil 153 | } 154 | 155 | // Release releases the IP which allocated to the container identified by id. 156 | func (e *Etcd) Release(id string) error { 157 | _, err := e.kv.Delete(context.TODO(), ipsPrefix + id) 158 | return err 159 | } 160 | 161 | // GatewayMap is the map of subnet and gateway, used by monkey 162 | type GatewayMap struct { 163 | Subnet string `json:"subnet"` 164 | Gateway string `json:"gw"` 165 | } 166 | 167 | // AllocateMap is the map of dedicated IPs and the namespace, used by monkey 168 | type AllocateMap struct { 169 | Allocate string `json:"ips"` 170 | Namespace string `json:"ns"` 171 | // TODO: Add label to support explicate which IPs to use for a given pods. 172 | // Label string `json:"label"` 173 | } 174 | 175 | // InUsedMap is the map of ContainerID and its IP, used by monkey 176 | type InUsedMap struct { 177 | ContainerID string `json:"id"` 178 | IP net.IP `json:"ip"` 179 | Pod string `json:"pod"` 180 | Namespace string `json:"ns"` 181 | App string `json:"app,omitempty"` 182 | Service string `json:"svc,omitempty"` 183 | } 184 | 185 | // AllGatewayMap gets all gateway map in the store 186 | func (e *Etcd) AllGatewayMap() (*[]GatewayMap, error) { 187 | gms := make([]GatewayMap, 0) 188 | resp, err := e.kv.Get(context.TODO(), gatewayPrefix, clientv3.WithPrefix()) 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | // s := make([]string, 0) 194 | for _, item := range resp.Kvs { 195 | subnet := strings.TrimPrefix(string(item.Key), gatewayPrefix) 196 | gw := string(item.Value) 197 | _, _, err := net.ParseCIDR(subnet) 198 | if err != nil { 199 | // ivalid format, just omit. 200 | continue 201 | } 202 | 203 | if net.ParseIP(gw) == nil { 204 | // ivalid format, just omit. 205 | continue 206 | } 207 | gms = append(gms, GatewayMap{ 208 | // Subnet: *subnet, 209 | // Gateway: gateway, 210 | Subnet: subnet, 211 | Gateway: gw, 212 | }) 213 | } 214 | return &gms, nil 215 | } 216 | 217 | // InsertGatewayMap inserts a gateway map 218 | func (e *Etcd) InsertGatewayMap(gm GatewayMap) error { 219 | if _, err := e.kv.Put(context.TODO(), gatewayPrefix+gm.Subnet, gm.Gateway); err != nil { 220 | 221 | return err 222 | } 223 | return nil 224 | } 225 | 226 | // DeleteGatewayMap deletes a gateway map 227 | func (e *Etcd) DeleteGatewayMap(gms []GatewayMap) error { 228 | for _, gm := range gms { 229 | // if _, err := e.kv.Delete(context.TODO(), gm.Subnet.String()); err != nil { 230 | if _, err := e.kv.Delete(context.TODO(), gatewayPrefix+gm.Subnet); err != nil { 231 | // TODO: error when delete one item, should we just stop and return error? 232 | // If we omit one error, maybe all errors are omitted. 233 | return err 234 | } 235 | } 236 | return nil 237 | } 238 | 239 | // RetrieveUsedbyNamespace retrieves used IP in subnet for namespace. 240 | func (e *Etcd) RetrieveUsedbyNamespace(namespace string, adminRole bool) (*[]string, error) { 241 | resp, err := e.kv.Get(context.TODO(), ipsPrefix, clientv3.WithPrefix()) 242 | if err != nil { 243 | return nil, err 244 | } 245 | // TODO: which return type is best? []string or RangeSet? 246 | // ret := make([]net.IP, 0) 247 | s := make([]string, 0) 248 | if adminRole { 249 | for _, item := range resp.Kvs { 250 | s = append(s, string(item.Value)) 251 | } 252 | } else { 253 | for _, item := range resp.Kvs { 254 | row := strings.Split(string(item.Value), ",") 255 | if row[2] == namespace { 256 | s = append(s, string(item.Value)) 257 | } 258 | } 259 | } 260 | return &s, nil 261 | } 262 | 263 | // AllAllocate gets all allocate map 264 | func (e *Etcd) AllAllocate() (*[]AllocateMap, error) { 265 | ams := make([]AllocateMap, 0) 266 | resp, err := e.kv.Get(context.TODO(), userPrefix, clientv3.WithPrefix()) 267 | if err != nil { 268 | return nil, err 269 | } 270 | 271 | // s := make([]string, 0) 272 | for _, item := range resp.Kvs { 273 | ns := strings.TrimPrefix(string(item.Key), userPrefix) 274 | allocate := string(item.Value) 275 | ams = append(ams, AllocateMap{ 276 | // Subnet: *subnet, 277 | // Gateway: gateway, 278 | Allocate: allocate, 279 | Namespace: ns, 280 | }) 281 | } 282 | return &ams, nil 283 | 284 | } 285 | 286 | // InsertAllocateMap inserts a allocate map 287 | func (e *Etcd) InsertAllocateMap(am AllocateMap) error { 288 | if _, err := e.kv.Put(context.TODO(), userPrefix+am.Namespace, am.Allocate); err != nil { 289 | 290 | return err 291 | } 292 | return nil 293 | } 294 | 295 | // DeleteAllocateMap deletes a allocate map 296 | func (e *Etcd) DeleteAllocateMap(ams []AllocateMap) error { 297 | for _, am := range ams { 298 | // if _, err := e.kv.Delete(context.TODO(), gm.Subnet.String()); err != nil { 299 | if _, err := e.kv.Delete(context.TODO(), userPrefix+am.Namespace); err != nil { 300 | // TODO: error when delete one item, should we just stop and return error? 301 | // If we omit one error, maybe all errors are omitted. 302 | return err 303 | } 304 | } 305 | return nil 306 | } 307 | -------------------------------------------------------------------------------- /pkg/store/store.go: -------------------------------------------------------------------------------- 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 store 9 | 10 | import ( 11 | "github.com/hainesc/anchor/pkg/utils" 12 | "net" 13 | ) 14 | 15 | // Store is the store interface for anchor 16 | type Store interface { 17 | Lock() error 18 | Unlock() error 19 | Close() error 20 | Reserve(id string, ip net.IP, podName string, podNamespace string, controller string) (bool, error) 21 | Release(id string) error 22 | 23 | RetrieveGateway(subnet *net.IPNet) net.IP // return nil if error 24 | RetrieveAllocated(namespace string, subnet *net.IPNet) (*utils.RangeSet, error) 25 | RetrieveUsed(namespace string, subnet *net.IPNet) (*utils.RangeSet, error) 26 | } 27 | -------------------------------------------------------------------------------- /pkg/utils/config.go: -------------------------------------------------------------------------------- 1 | package utils 2 | -------------------------------------------------------------------------------- /pkg/utils/range.go: -------------------------------------------------------------------------------- 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 utils 9 | 10 | import ( 11 | "fmt" 12 | "net" 13 | 14 | "github.com/containernetworking/plugins/pkg/ip" 15 | ) 16 | 17 | // Range represents a IP range 18 | type Range struct { 19 | RangeStart net.IP 20 | RangeEnd net.IP 21 | Subnet net.IPNet 22 | Gateway net.IP 23 | } 24 | 25 | // Canonicalize takes a given range and ensures that all information is consistent, 26 | // filling out Start, End, and Gateway with sane values if missing 27 | func (r *Range) Canonicalize() error { 28 | if err := canonicalizeIP(&r.Subnet.IP); err != nil { 29 | return err 30 | } 31 | 32 | // Can't create an allocator for a network with no addresses, eg 33 | // a /32 or /31 34 | ones, masklen := r.Subnet.Mask.Size() 35 | if ones > masklen-2 { 36 | return fmt.Errorf("Network %s too small to allocate from", (*net.IPNet)(&r.Subnet).String()) 37 | } 38 | 39 | if len(r.Subnet.IP) != len(r.Subnet.Mask) { 40 | return fmt.Errorf("IPNet IP and Mask version mismatch") 41 | } 42 | 43 | // Ensure Subnet IP is the network address, not some other address 44 | networkIP := r.Subnet.IP.Mask(r.Subnet.Mask) 45 | if !r.Subnet.IP.Equal(networkIP) { 46 | return fmt.Errorf("Network has host bits set. For a subnet mask of length %d the network address is %s", ones, networkIP.String()) 47 | } 48 | 49 | // If the gateway is nil, claim .1 50 | if r.Gateway == nil { 51 | // TODO: remove this piece of code, if not gateway, say error. 52 | r.Gateway = ip.NextIP(r.Subnet.IP) 53 | } else { 54 | if err := canonicalizeIP(&r.Gateway); err != nil { 55 | return err 56 | } 57 | subnet := (net.IPNet)(r.Subnet) 58 | if !subnet.Contains(r.Gateway) { 59 | return fmt.Errorf("gateway %s not in network %s", r.Gateway.String(), subnet.String()) 60 | } 61 | } 62 | 63 | // RangeStart: If specified, make sure it's sane (inside the subnet), 64 | // otherwise use the first free IP (i.e. .1) - this will conflict with the 65 | // gateway but we skip it in the iterator 66 | if r.RangeStart != nil { 67 | if err := canonicalizeIP(&r.RangeStart); err != nil { 68 | return err 69 | } 70 | 71 | if !r.Contains(r.RangeStart) { 72 | return fmt.Errorf("RangeStart %s not in network %s", r.RangeStart.String(), (*net.IPNet)(&r.Subnet).String()) 73 | } 74 | } else { 75 | r.RangeStart = ip.NextIP(r.Subnet.IP) 76 | } 77 | 78 | // RangeEnd: If specified, verify sanity. Otherwise, add a sensible default 79 | // (e.g. for a /24: .254 if IPv4, ::255 if IPv6) 80 | if r.RangeEnd != nil { 81 | if err := canonicalizeIP(&r.RangeEnd); err != nil { 82 | return err 83 | } 84 | 85 | if !r.Contains(r.RangeEnd) { 86 | return fmt.Errorf("RangeEnd %s not in network %s", r.RangeEnd.String(), (*net.IPNet)(&r.Subnet).String()) 87 | } 88 | } else { 89 | r.RangeEnd = lastIP(r.Subnet) 90 | } 91 | 92 | return nil 93 | } 94 | 95 | // Contains checks if a given ip contained in the range 96 | func (r *Range) Contains(addr net.IP) bool { 97 | if err := canonicalizeIP(&addr); err != nil { 98 | return false 99 | } 100 | 101 | subnet := (net.IPNet)(r.Subnet) 102 | 103 | // Not the same address family 104 | if len(addr) != len(r.Subnet.IP) { 105 | return false 106 | } 107 | 108 | // Not in network 109 | if !subnet.Contains(addr) { 110 | return false 111 | } 112 | 113 | // We ignore nils here so we can use this function as we initialize the range. 114 | if r.RangeStart != nil { 115 | // Before the range start 116 | if ip.Cmp(addr, r.RangeStart) < 0 { 117 | return false 118 | } 119 | } 120 | 121 | if r.RangeEnd != nil { 122 | if ip.Cmp(addr, r.RangeEnd) > 0 { 123 | // After the range end 124 | return false 125 | } 126 | } 127 | 128 | return true 129 | } 130 | 131 | // Overlaps returns true if there is any overlap between ranges 132 | func (r *Range) Overlaps(r1 *Range) bool { 133 | // different familes 134 | if len(r.RangeStart) != len(r1.RangeStart) { 135 | return false 136 | } 137 | 138 | return r.Contains(r1.RangeStart) || 139 | r.Contains(r1.RangeEnd) || 140 | r1.Contains(r.RangeStart) || 141 | r1.Contains(r.RangeEnd) 142 | } 143 | 144 | // String prints the range 145 | func (r *Range) String() string { 146 | return fmt.Sprintf("%s-%s", r.RangeStart.String(), r.RangeEnd.String()) 147 | } 148 | 149 | // canonicalizeIP makes sure a provided ip is in standard form 150 | func canonicalizeIP(ip *net.IP) error { 151 | if ip.To4() != nil { 152 | *ip = ip.To4() 153 | return nil 154 | } else if ip.To16() != nil { 155 | *ip = ip.To16() 156 | return nil 157 | } 158 | return fmt.Errorf("IP %s not v4 nor v6", *ip) 159 | } 160 | 161 | // Determine the last IP of a subnet, excluding the broadcast if IPv4 162 | func lastIP(subnet net.IPNet) net.IP { 163 | var end net.IP 164 | for i := 0; i < len(subnet.IP); i++ { 165 | end = append(end, subnet.IP[i]|^subnet.Mask[i]) 166 | } 167 | if subnet.IP.To4() != nil { 168 | end[3]-- 169 | } 170 | 171 | return end 172 | } 173 | -------------------------------------------------------------------------------- /pkg/utils/range_set.go: -------------------------------------------------------------------------------- 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 utils 9 | 10 | import ( 11 | "fmt" 12 | "github.com/containernetworking/plugins/pkg/ip" 13 | "net" 14 | "sort" 15 | "strings" 16 | ) 17 | 18 | // RangeSet is a array of Range 19 | type RangeSet []Range 20 | 21 | // RangeIter is a iterator of a RangeSet 22 | type RangeIter struct { 23 | rangeset *RangeSet 24 | 25 | // The current range id 26 | rangeIdx int 27 | 28 | // Our current position 29 | cur net.IP 30 | 31 | // The IP and range index where we started iterating; if we hit this again, we're done. 32 | startIP net.IP 33 | startRange int 34 | } 35 | 36 | // Next returns the next IP, its mask, and its gateway. Returns nil 37 | // if the iterator has been exhausted 38 | func (i *RangeIter) Next() (*net.IPNet, net.IP) { 39 | r := (*i.rangeset)[i.rangeIdx] 40 | 41 | // If this is the first time iterating and we're not starting in the middle 42 | // of the range, then start at rangeStart, which is inclusive 43 | if i.cur == nil { 44 | i.cur = r.RangeStart 45 | i.startIP = i.cur 46 | if i.cur.Equal(r.Gateway) { 47 | return i.Next() 48 | } 49 | return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway 50 | } 51 | 52 | // If we've reached the end of this range, we need to advance the range 53 | // RangeEnd is inclusive as well 54 | if i.cur.Equal(r.RangeEnd) { 55 | i.rangeIdx++ 56 | i.rangeIdx %= len(*i.rangeset) 57 | r = (*i.rangeset)[i.rangeIdx] 58 | 59 | i.cur = r.RangeStart 60 | } else { 61 | i.cur = ip.NextIP(i.cur) 62 | } 63 | 64 | if i.startIP == nil { 65 | i.startIP = i.cur 66 | } else if i.rangeIdx == i.startRange && i.cur.Equal(i.startIP) { 67 | // IF we've looped back to where we started, give up 68 | return nil, nil 69 | } 70 | 71 | if i.cur.Equal(r.Gateway) { 72 | return i.Next() 73 | } 74 | 75 | return &net.IPNet{IP: i.cur, Mask: r.Subnet.Mask}, r.Gateway 76 | } 77 | 78 | // Contains returns true if any range in this set contains an IP 79 | func (rs *RangeSet) Contains(addr net.IP) bool { 80 | r, _ := rs.RangeFor(addr) 81 | return r != nil 82 | } 83 | 84 | // RangeFor finds the range that contains an IP, or nil if not found 85 | func (rs *RangeSet) RangeFor(addr net.IP) (*Range, error) { 86 | if err := canonicalizeIP(&addr); err != nil { 87 | return nil, err 88 | } 89 | 90 | for _, r := range *rs { 91 | if r.Contains(addr) { 92 | return &r, nil 93 | } 94 | } 95 | 96 | return nil, fmt.Errorf("%s not in range set %s", addr.String(), rs.String()) 97 | } 98 | 99 | // Overlaps returns true if any ranges in any set overlap with this one 100 | func (rs *RangeSet) Overlaps(p1 *RangeSet) bool { 101 | for _, r := range *rs { 102 | for _, r1 := range *p1 { 103 | if r.Overlaps(&r1) { 104 | return true 105 | } 106 | } 107 | } 108 | return false 109 | } 110 | 111 | // Canonicalize ensures the RangeSet is in a standard form, and detects any 112 | // invalid input. Call Range.Canonicalize() on every Range in the set 113 | func (rs *RangeSet) Canonicalize() error { 114 | if len(*rs) == 0 { 115 | return fmt.Errorf("empty range set") 116 | } 117 | 118 | fam := 0 119 | for i := range *rs { 120 | if err := (*rs)[i].Canonicalize(); err != nil { 121 | return err 122 | } 123 | if i == 0 { 124 | fam = len((*rs)[i].RangeStart) 125 | } else { 126 | if fam != len((*rs)[i].RangeStart) { 127 | return fmt.Errorf("mixed address families") 128 | } 129 | } 130 | } 131 | 132 | // Make sure none of the ranges in the set overlap 133 | l := len(*rs) 134 | for i, r1 := range (*rs)[:l-1] { 135 | for _, r2 := range (*rs)[i+1:] { 136 | if r1.Overlaps(&r2) { 137 | return fmt.Errorf("subnets %s and %s overlap", r1.String(), r2.String()) 138 | } 139 | } 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func (rs *RangeSet) String() string { 146 | out := []string{} 147 | for _, r := range *rs { 148 | out = append(out, r.String()) 149 | } 150 | 151 | return strings.Join(out, ",") 152 | } 153 | 154 | // Concat concats RangeSet from string for given subnet. 155 | // eg: "10.0.0.[2-4], 10.0.1.4, 10.0.1.5, 10.0.1.9", 10.0.1.0/24 156 | func (rs *RangeSet) Concat(s string, subnet *net.IPNet) (*RangeSet, error) { 157 | // TODO: the code is designed for ipv4, what will happens when ipv6? 158 | // No special case when s is empty or rs is empty. 159 | if s == "" || strings.TrimSpace(s) == "" { 160 | return rs, nil 161 | } 162 | ranges := strings.Split(s, ",") 163 | for _, r := range ranges { 164 | // Remove all lead blanks and tailed blanks. 165 | r = strings.TrimSpace(r) 166 | if strings.HasSuffix(r, "]") { 167 | // eg: ["10.0.1.", "4-8"] 168 | segments := strings.Split(strings.TrimSuffix(r, "]"), "[") 169 | suffixs := strings.Split(segments[1], "-") 170 | start := net.ParseIP(segments[0] + suffixs[0]) 171 | if !subnet.Contains(start) { 172 | // This range don't belong to the subnet, so continue here. 173 | continue 174 | } 175 | end := net.ParseIP(segments[0] + suffixs[1]) 176 | if start == nil || end == nil { 177 | return nil, fmt.Errorf("invalid IP ranges %s", r) 178 | } 179 | 180 | *rs = append(*rs, Range{ 181 | RangeStart: start, 182 | RangeEnd: end, 183 | Subnet: *subnet, 184 | }) 185 | } else { 186 | // eg: 10.1.8.9 187 | current := net.ParseIP(r) 188 | if current == nil { 189 | return nil, fmt.Errorf("invalid IP ranges %s", r) 190 | } 191 | if !subnet.Contains(current) { 192 | // This range don't belong to the subnet, so continue here. 193 | continue 194 | } 195 | *rs = append(*rs, Range{ 196 | RangeStart: current, 197 | RangeEnd: current, 198 | Subnet: *subnet, 199 | }) 200 | } 201 | } 202 | sort.Sort(rs) 203 | 204 | cursor := 0 205 | l := rs.Len() 206 | i := 1 207 | for ; i < l; i++ { 208 | // A gap here 209 | if ip.Cmp((*rs)[cursor].RangeEnd, (*rs)[i].RangeStart) < 0 && !(*rs)[i].RangeStart.Equal(ip.NextIP((*rs)[cursor].RangeEnd)) { 210 | if i > cursor+1 { 211 | *rs = append((*rs)[:cursor+1], (*rs)[i:]...) 212 | gap := i - cursor - 1 213 | l -= gap 214 | i -= gap 215 | 216 | } 217 | cursor++ 218 | } else { // overlap case 219 | if ip.Cmp((*rs)[i].RangeEnd, (*rs)[cursor].RangeEnd) > 0 { 220 | (*rs)[cursor].RangeEnd = (*rs)[i].RangeEnd 221 | } 222 | } 223 | } 224 | 225 | if i > cursor+1 { 226 | *rs = append((*rs)[:cursor+1], (*rs)[i:]...) 227 | } 228 | return rs, nil 229 | } 230 | 231 | func (rs RangeSet) Len() int { 232 | return len(rs) 233 | } 234 | 235 | func (rs RangeSet) Swap(i, j int) { 236 | rs[i], rs[j] = rs[j], rs[i] 237 | } 238 | 239 | func (rs RangeSet) Less(i, j int) bool { 240 | a, b := rs[i], rs[j] 241 | 242 | if ip.Cmp(a.RangeStart, b.RangeStart) != 0 { 243 | return ip.Cmp(a.RangeStart, b.RangeStart) < 0 244 | } 245 | return ip.Cmp(a.RangeEnd, b.RangeEnd) < 0 246 | } 247 | -------------------------------------------------------------------------------- /pkg/utils/range_set_test.go: -------------------------------------------------------------------------------- 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 utils 9 | 10 | import ( 11 | "net" 12 | "strings" 13 | "testing" 14 | ) 15 | 16 | func Test_Contat(t *testing.T) { 17 | _, subnet, _ := net.ParseCIDR("10.0.1.0/24") 18 | 19 | s := make([]string, 0) 20 | origin := RangeSet{} 21 | t.Log("testing empty string") 22 | if _, err := origin.Concat("", subnet); err != nil { 23 | t.Fatal(err.Error()) 24 | } 25 | t.Log("testing empty map") 26 | if _, err := origin.Concat(strings.Join(s, ","), subnet); err != nil { 27 | 28 | t.Fatal(err.Error()) 29 | } 30 | t.Log("test succuss") 31 | } 32 | -------------------------------------------------------------------------------- /scripts/install-cni.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Script to install Anchor CNI on a Kubernetes host. 4 | # - Expects the host CNI binary path to be mounted at /host/opt/cni/bin. 5 | # - Expects the host CNI network config path to be mounted at /host/etc/cni/net.d. 6 | # - Expects the desired CNI config in the CNI_NETWORK_CONFIG env variable. 7 | 8 | # Ensure all variables are defined, and that the script fails when an error is hit. 9 | set -u -e 10 | 11 | # Capture the usual signals and exit from the script 12 | trap 'echo "SIGINT received, simply exiting..."; exit 0' SIGINT 13 | trap 'echo "SIGTERM received, simply exiting..."; exit 0' SIGTERM 14 | trap 'echo "SIGHUP received, simply exiting..."; exit 0' SIGHUP 15 | 16 | OCTOPUS="" 17 | NODE_IPS="" 18 | # Create macvlan interface 19 | if [ "$CREATE_MACVLAN" == "true" ]; then 20 | OIFS=$IFS 21 | IFS=";" 22 | hostname=$(hostname) 23 | # Remove all spaces in the CLUSTER_NETWORK 24 | CLUSTER_NETWORK=${CLUSTER_NETWORK//[[:blank:]]/} 25 | suffix=0 26 | # hostname, master, ip, gateway, mask 27 | for t in $CLUSTER_NETWORK; do 28 | if [ "$hostname" == "$(echo $t | cut -d',' -f1)" ]; then 29 | # TODO: check invalidation of the inputs. 30 | master="$(echo $t | cut -d',' -f2)" 31 | ip="$(echo $t | cut -d',' -f3)" 32 | gateway="$(echo $t | cut -d',' -f4)" 33 | mask="$(echo $t | cut -d',' -f5)" 34 | a=$(echo $ip | cut -d'.' -f1) 35 | b=$(echo $ip | cut -d'.' -f2) 36 | c=$(echo $ip | cut -d'.' -f3) 37 | d=$(echo $ip | cut -d'.' -f4) 38 | ip_int="$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))" 39 | subnet_int=$(($ip_int & (0xffffffff - (1<<32-$mask) + 1))) 40 | 41 | delim="" 42 | subnet="" 43 | # Caculate the subnet. 44 | for e in 3 2 1 0; do 45 | octet=$(($subnet_int / (256 ** $e))) 46 | subnet_int=$((subnet_int -= octet * 256 ** $e)) 47 | subnet=$subnet$delim$octet 48 | delim=. 49 | done 50 | subnet=$subnet/$mask 51 | # Restore the IFS. 52 | IFS=";" 53 | NODE_IPS=$NODE_IPS\"$ip\", 54 | OCTOPUS=$OCTOPUS\"${subnet}\":\"${master}\", 55 | MACVLAN_INTERFACE=$master 56 | noskip=false 57 | if [ ${#suffix} -gt 2 ]; then 58 | echo "Max 100 interfaces are support" && exit 1 59 | fi 60 | if [ ${#suffix} -eq 1 ]; then 61 | macvlan=acr0"$suffix" 62 | else 63 | macvlan=acr"$suffix" 64 | fi 65 | ip addr | grep -oE "${macvlan}" > /dev/null 2>&1 || noskip=true 66 | if [ "$noskip" == "true" ]; then 67 | echo "Turnning $master promisc on..." 68 | ip link set $master promisc on 69 | echo "Creating macvlan interface..." 70 | interface_created=false 71 | if [ $interface_created == "false" ]; then 72 | # We write in this way since we have set -eu in the header of this script. 73 | interface_created=true 74 | ip link add $macvlan link $master type macvlan mode bridge > /dev/null 2>&1 || interface_created=false 75 | suffix=$((suffix+1)) 76 | fi 77 | if [ $interface_created == "false" ]; then 78 | echo "Cannot create macvlan interface, will exit soon" 79 | ip link set dev $master up 80 | exit 1 81 | fi 82 | 83 | ip link set dev $master up 84 | 85 | echo "Deleting $ip from device $master..." 86 | ip addr del $ip/$mask dev $master || true 87 | 88 | echo "Adding $ip to device $macvlan..." 89 | ip addr add $ip/$mask dev $macvlan 90 | 91 | echo "Turnning on $macvlan and flushing the route infomation..." 92 | ip link set dev $macvlan up 93 | ip route flush dev $macvlan 94 | 95 | echo "Replacing the route for $subnet..." 96 | ip route replace $subnet dev $macvlan metric 0 97 | 98 | # Only change the route for default at the first interface. 99 | if [ $macvlan == "acr00" ]; then 100 | echo "Replacing the route for default..." 101 | ip route add default via $gateway dev $macvlan || \ 102 | ip route replace default via $gateway dev $macvlan || true 103 | fi 104 | # Ping the gateway for fast flushing the cache on the switch. 105 | ping -c 4 $gateway > /dev/null 2>&1 || true 106 | else 107 | echo "MacVLAN insterface ${macvlan} for anchor exists, Check the information below: " 108 | echo "" 109 | echo "Hostname: $hostname" 110 | echo "" 111 | ip addr | grep -oE "${macvlan}@$master" | grep -oE "${macvlan}" | xargs -n 1 ip addr show 112 | 113 | echo "" 114 | echo "It may be caused by: " 115 | echo " 1. This pod which belongs to anchor daemonset get killed and restart" 116 | echo " 2. Somebody create ${macvlan} manaully" 117 | echo " 3. Something error in pre-installation and then re-deploy anchor" 118 | echo " 4. The ${macvlan} interface is remains by the old env" 119 | echo "" 120 | echo "What you can do are: " 121 | echo " When 1. Nothing to do" 122 | echo " When 2. Delete it manaully Or it will be deleted by node restart" 123 | echo " When 3. Nothing to do if cluster network not changed, else restart the node" 124 | echo " When 4. Restart the node" 125 | suffix=$((suffix+1)) 126 | fi 127 | fi 128 | done 129 | IFS=$OIFS 130 | fi 131 | 132 | # The directory on the host where CNI networks are installed. Defaults to 133 | # /etc/cni/net.d, but can be overridden by setting CNI_NET_DIR. This is used 134 | # for populating absolute paths in the CNI network config to assets 135 | # which are installed in the CNI network config directory. 136 | HOST_CNI_NET_DIR=${CNI_NET_DIR:-/etc/cni/net.d} 137 | HOST_SECRETS_DIR=${HOST_CNI_NET_DIR}/anchor-tls 138 | 139 | # Directory where we expect that TLS assets will be mounted into 140 | # the anchor/cni container. 141 | SECRETS_MOUNT_DIR=${TLS_ASSETS_DIR:-/anchor-secrets} 142 | 143 | # Clean up any existing binaries / config / assets. 144 | rm -f /host/opt/cni/bin/anchor-ipam 145 | rm -f /host/etc/cni/net.d/anchor-tls/* 146 | 147 | # Copy over any TLS assets from the SECRETS_MOUNT_DIR to the host. 148 | # First check if the dir exists and has anything in it. 149 | if [ "$(ls ${SECRETS_MOUNT_DIR} 3>/dev/null)" ]; 150 | then 151 | echo "Installing any TLS assets from ${SECRETS_MOUNT_DIR}" 152 | mkdir -p /host/etc/cni/net.d/anchor-tls 153 | cp -p ${SECRETS_MOUNT_DIR}/* /host/etc/cni/net.d/anchor-tls/ 154 | fi 155 | 156 | # If the TLS assets actually exist, update the variables to populate into the 157 | # CNI network config. Otherwise, we'll just fill that in with blanks. 158 | if [ -e "/host/etc/cni/net.d/anchor-tls/etcd-ca" ]; 159 | then 160 | CNI_CONF_ETCD_CA=${HOST_SECRETS_DIR}/etcd-ca 161 | fi 162 | 163 | if [ -e "/host/etc/cni/net.d/anchor-tls/etcd-key" ]; 164 | then 165 | CNI_CONF_ETCD_KEY=${HOST_SECRETS_DIR}/etcd-key 166 | fi 167 | 168 | if [ -e "/host/etc/cni/net.d/anchor-tls/etcd-cert" ]; 169 | then 170 | CNI_CONF_ETCD_CERT=${HOST_SECRETS_DIR}/etcd-cert 171 | fi 172 | 173 | # Choose which default cni binaries should be copied 174 | SKIP_CNI_BINARIES=${SKIP_CNI_BINARIES:-""} 175 | SKIP_CNI_BINARIES=",$SKIP_CNI_BINARIES," 176 | UPDATE_CNI_BINARIES=${UPDATE_CNI_BINARIES:-"true"} 177 | 178 | # Place the new binaries if the directory is writeable. 179 | for dir in /host/opt/cni/bin /host/secondary-bin-dir 180 | do 181 | if [ ! -w "$dir" ]; 182 | then 183 | echo "$dir is non-writeable, skipping" 184 | continue 185 | fi 186 | for path in /opt/cni/bin/*; 187 | do 188 | filename="$(basename $path)" 189 | tmp=",$filename," 190 | if [ "${SKIP_CNI_BINARIES#*$tmp}" != "$SKIP_CNI_BINARIES" ]; 191 | then 192 | echo "$filename is in SKIP_CNI_BINARIES, skipping" 193 | continue 194 | fi 195 | if [ "${UPDATE_CNI_BINARIES}" != "true" -a -f $dir/$filename ]; 196 | then 197 | echo "$dir/$filename is already here and UPDATE_CNI_BINARIES isn't true, skipping" 198 | continue 199 | fi 200 | cp $path $dir/ 201 | if [ "$?" != "0" ]; 202 | then 203 | echo "Failed to copy $path to $dir. This may be caused by selinux configuration on the host, or something else." 204 | exit 1 205 | fi 206 | done 207 | 208 | echo "Wrote Anchor CNI binaries to $dir" 209 | # TODO: log version. 210 | done 211 | 212 | TMP_CONF='/anchor.conf.tmp' 213 | # If specified, overwrite the network configuration file. 214 | if [ "${CNI_NETWORK_CONFIG:-}" != "" ]; then 215 | cat >$TMP_CONF < /host/etc/cni/net.d/anchor-kubeconfig </dev/null)" ]; 325 | then 326 | stat_output=$(stat -c%y ${SECRETS_MOUNT_DIR}/etcd-cert 2>/dev/null) 327 | sleep 10; 328 | if [ "$stat_output" != "$(stat -c%y ${SECRETS_MOUNT_DIR}/etcd-cert 2>/dev/null)" ]; then 329 | echo "Updating installed secrets at: $(date)" 330 | cp -p ${SECRETS_MOUNT_DIR}/* /host/etc/cni/net.d/anchor-tls/ 331 | fi 332 | else 333 | sleep 10 334 | fi 335 | done 336 | 337 | --------------------------------------------------------------------------------