*: add peer VPN support

This commit adds support for defining arbitrary peers that should have
access to the VPN. In k8s, this is accomplished using the new Peer CRD.
This commit is contained in:
Lucas Servén Marín 2019-05-03 12:53:40 +02:00
parent 46f55c337b
commit 2425a06cd8
No known key found for this signature in database
GPG Key ID: 586FEAF680DA74AD
47 changed files with 15812 additions and 505 deletions

103
Makefile
View File

@ -1,5 +1,5 @@
export GO111MODULE=on
.PHONY: all push container clean container-name container-latest push-latest fmt lint test unit vendor header
#export GO111MODULE=on
.PHONY: all push container clean container-name container-latest push-latest fmt lint test unit vendor header generate client deepcopy informer lister openapi
BINS := $(addprefix bin/,kg kgctl)
PROJECT := kilo
@ -22,12 +22,88 @@ SRC := $(shell find . -type f -name '*.go' -not -path "./vendor/*")
GO_FILES ?= $$(find . -name '*.go' -not -path './vendor/*')
GO_PKGS ?= $$(go list ./... | grep -v "$(PKG)/vendor")
CLIENT_GO_VERSION := release-11.0
CODE_GENERATOR_VERSION := release-1.14
KUBE_OPENAPI_VERSION := b3a7cee44
CLIENT_GEN_BINARY:=$(GOPATH)/bin/client-gen
DEEPCOPY_GEN_BINARY:=$(GOPATH)/bin/deepcopy-gen
INFORMER_GEN_BINARY:=$(GOPATH)/bin/informer-gen
LISTER_GEN_BINARY:=$(GOPATH)/bin/lister-gen
OPENAPI_GEN_BINARY:=$(GOPATH)/bin/openapi-gen
BUILD_IMAGE ?= golang:1.12.1-alpine
all: build
build: $(BINS)
generate: client deepcopy informer lister openapi
client: pkg/k8s/clientset/versioned/typed/kilo/v1alpha1/peer.go
pkg/k8s/clientset/versioned/typed/kilo/v1alpha1/peer.go: .header pkg/k8s/apis/kilo/v1alpha1/types.go $(CLIENT_GEN_BINARY)
$(CLIENT_GEN_BINARY) \
--clientset-name versioned \
--input-base "" \
--input $(PKG)/pkg/k8s/apis/kilo/v1alpha1 \
--output-base $(CURDIR) \
--output-package $(PKG)/pkg/k8s/clientset \
--go-header-file=.header \
--logtostderr
rm -r pkg/k8s/clientset
mv $(PKG)/pkg/k8s/clientset pkg/k8s
rm -r github.com
go fmt ./pkg/k8s/clientset/...
deepcopy: pkg/k8s/apis/kilo/v1alpha1/zz_generated.deepcopy.go
pkg/k8s/apis/kilo/v1alpha1/zz_generated.deepcopy.go: .header pkg/k8s/apis/kilo/v1alpha1/types.go $(DEEPCOPY_GEN_BINARY)
$(DEEPCOPY_GEN_BINARY) \
--input-dirs ./$(@D) \
--go-header-file=.header \
--logtostderr \
--output-base $(CURDIR) \
--output-file-base zz_generated.deepcopy \
go fmt $@
informer: pkg/k8s/informers/kilo/v1alpha1/peer.go
pkg/k8s/informers/kilo/v1alpha1/peer.go: .header pkg/k8s/apis/kilo/v1alpha1/types.go $(INFORMER_GEN_BINARY)
$(INFORMER_GEN_BINARY) \
--input-dirs $(PKG)/pkg/k8s/apis/kilo/v1alpha1 \
--go-header-file=.header \
--logtostderr \
--versioned-clientset-package $(PKG)/pkg/k8s/clientset/versioned \
--listers-package $(PKG)/pkg/k8s/listers \
--output-base $(CURDIR) \
--output-package $(PKG)/pkg/k8s/informers \
--single-directory
rm -r pkg/k8s/informers
mv $(PKG)/pkg/k8s/informers pkg/k8s
rm -r github.com
go fmt ./pkg/k8s/informers/...
lister: pkg/k8s/listers/kilo/v1alpha1/peer.go
pkg/k8s/listers/kilo/v1alpha1/peer.go: .header pkg/k8s/apis/kilo/v1alpha1/types.go $(LISTER_GEN_BINARY)
$(LISTER_GEN_BINARY) \
--input-dirs $(PKG)/pkg/k8s/apis/kilo/v1alpha1 \
--go-header-file=.header \
--logtostderr \
--output-base $(CURDIR) \
--output-package $(PKG)/pkg/k8s/listers
rm -r pkg/k8s/listers
mv $(PKG)/pkg/k8s/listers pkg/k8s
rm -r github.com
go fmt ./pkg/k8s/listers/...
openapi: pkg/k8s/apis/kilo/v1alpha1/openapi_generated.go
pkg/k8s/apis/kilo/v1alpha1/openapi_generated.go: pkg/k8s/apis/kilo/v1alpha1/types.go $(OPENAPI_GEN_BINARY)
$(OPENAPI_GEN_BINARY) \
--input-dirs ./$(@D),k8s.io/apimachinery/pkg/apis/meta/v1,k8s.io/api/core/v1 \
--output-base $(CURDIR) \
--output-package ./$(@D) \
--logtostderr \
--report-filename /dev/null \
--go-header-file=.header
go fmt $@
$(BINS): $(SRC) go.mod
@mkdir -p bin
@echo "building: $@"
@ -82,10 +158,14 @@ test: lint unit
header: .header
@HEADER=$$(sed "s/YEAR/$$(date '+%Y')/" .header); \
HEADER_LEN=$$(wc -l .header | awk '{print $$1}'); \
FILES=; \
for f in $(GO_FILES); do \
FILE=$$(head -n $$(wc -l .header | awk '{print $$1}') $$f); \
[ "$$FILE" != "$$HEADER" ] && FILES="$$FILES$$f "; \
for i in 0 1 2 3 4 5; do \
FILE=$$(tail -n +$$i $$f | head -n $$HEADER_LEN); \
[ "$$FILE" = "$$HEADER" ] && continue 2; \
done; \
FILES="$$FILES$$f "; \
done; \
if [ -n "$$FILES" ]; then \
printf 'the following files are missing the license header: %s\n' "$$FILES"; \
@ -128,3 +208,18 @@ bin-clean:
vendor:
go mod tidy
go mod vendor
$(CLIENT_GEN_BINARY):
go get k8s.io/code-generator/cmd/client-gen@$(CODE_GENERATOR_VERSION)
$(DEEPCOPY_GEN_BINARY):
go get k8s.io/code-generator/cmd/deepcopy-gen@$(CODE_GENERATOR_VERSION)
$(INFORMER_GEN_BINARY):
go get k8s.io/code-generator/cmd/informer-gen@$(CODE_GENERATOR_VERSION)
$(LISTER_GEN_BINARY):
go get k8s.io/code-generator/cmd/lister-gen@$(CODE_GENERATOR_VERSION)
$(OPENAPI_GEN_BINARY):
go get k8s.io/kube-openapi/cmd/openapi-gen@$(KUBE_OPENAPI_VERSION)

View File

@ -30,10 +30,12 @@ import (
"github.com/oklog/run"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
apiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"github.com/squat/kilo/pkg/k8s"
kiloclient "github.com/squat/kilo/pkg/k8s/clientset/versioned"
"github.com/squat/kilo/pkg/mesh"
"github.com/squat/kilo/pkg/version"
)
@ -81,7 +83,8 @@ func Main() error {
local := flag.Bool("local", true, "Should Kilo manage routes within a location.")
logLevel := flag.String("log-level", logLevelInfo, fmt.Sprintf("Log level to use. Possible values: %s", availableLogLevels))
master := flag.String("master", "", "The address of the Kubernetes API server (overrides any value in kubeconfig).")
port := flag.Int("port", 51820, "The port over which WireGuard peers should communicate.")
var port uint
flag.UintVar(&port, "port", mesh.DefaultKiloPort, "The port over which WireGuard peers should communicate.")
subnet := flag.String("subnet", "10.4.0.0/16", "CIDR from which to allocate addresses for WireGuard interfaces.")
printVersion := flag.Bool("version", false, "Print version and exit")
flag.Parse()
@ -148,13 +151,15 @@ func Main() error {
if err != nil {
return fmt.Errorf("failed to create Kubernetes config: %v", err)
}
client := kubernetes.NewForConfigOrDie(config)
b = k8s.New(client)
c := kubernetes.NewForConfigOrDie(config)
kc := kiloclient.NewForConfigOrDie(config)
ec := apiextensions.NewForConfigOrDie(config)
b = k8s.New(c, kc, ec)
default:
return fmt.Errorf("backend %v unknown; possible values are: %s", *backend, availableBackends)
}
m, err := mesh.New(b, e, gr, *hostname, *port, s, *local, log.With(logger, "component", "kilo"))
m, err := mesh.New(b, e, gr, *hostname, uint32(port), s, *local, log.With(logger, "component", "kilo"))
if err != nil {
return fmt.Errorf("failed to create Kilo mesh: %v", err)
}

View File

@ -21,7 +21,7 @@ import (
"github.com/squat/kilo/pkg/mesh"
)
func newGraph() *cobra.Command {
func graph() *cobra.Command {
return &cobra.Command{
Use: "graph",
Short: "Generates a graph of the Kilo network",
@ -31,7 +31,7 @@ func newGraph() *cobra.Command {
}
func runGraph(_ *cobra.Command, _ []string) error {
ns, err := opts.backend.List()
ns, err := opts.backend.Nodes().List()
if err != nil {
return fmt.Errorf("failed to list nodes: %v", err)
}
@ -46,7 +46,7 @@ func runGraph(_ *cobra.Command, _ []string) error {
if len(nodes) == 0 {
return fmt.Errorf("did not find any valid Kilo nodes in the cluster")
}
t, err := mesh.NewTopology(nodes, opts.granularity, hostname, 0, []byte{}, opts.subnet)
t, err := mesh.NewTopology(nodes, nil, opts.granularity, hostname, 0, []byte{}, opts.subnet)
if err != nil {
return fmt.Errorf("failed to create topology: %v", err)
}

View File

@ -20,11 +20,13 @@ import (
"os"
"strings"
"github.com/spf13/cobra"
apiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/tools/clientcmd"
"github.com/spf13/cobra"
"github.com/squat/kilo/pkg/k8s"
kiloclient "github.com/squat/kilo/pkg/k8s/clientset/versioned"
"github.com/squat/kilo/pkg/mesh"
"github.com/squat/kilo/pkg/version"
)
@ -86,14 +88,20 @@ func runRoot(_ *cobra.Command, _ []string) error {
if err != nil {
return fmt.Errorf("failed to create Kubernetes config: %v", err)
}
client := kubernetes.NewForConfigOrDie(config)
opts.backend = k8s.New(client)
c := kubernetes.NewForConfigOrDie(config)
kc := kiloclient.NewForConfigOrDie(config)
ec := apiextensions.NewForConfigOrDie(config)
opts.backend = k8s.New(c, kc, ec)
default:
return fmt.Errorf("backend %v unknown; posible values are: %s", backend, availableBackends)
}
if err := opts.backend.Init(make(chan struct{})); err != nil {
return fmt.Errorf("failed to initialize backend: %v", err)
if err := opts.backend.Nodes().Init(make(chan struct{})); err != nil {
return fmt.Errorf("failed to initialize node backend: %v", err)
}
if err := opts.backend.Peers().Init(make(chan struct{})); err != nil {
return fmt.Errorf("failed to initialize peer backend: %v", err)
}
return nil
}
@ -112,7 +120,8 @@ func main() {
cmd.PersistentFlags().StringVar(&subnet, "subnet", "10.4.0.0/16", "CIDR from which to allocate addressees to WireGuard interfaces.")
for _, subCmd := range []*cobra.Command{
newGraph(),
graph(),
showConf(),
} {
cmd.AddCommand(subCmd)
}

146
cmd/kgctl/showconf.go Normal file
View File

@ -0,0 +1,146 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"errors"
"fmt"
"github.com/spf13/cobra"
"github.com/squat/kilo/pkg/mesh"
)
func showConf() *cobra.Command {
cmd := &cobra.Command{
Use: "showconf",
Short: "Show the WireGuard configuration for a node or peer in the Kilo network",
Long: "",
}
for _, subCmd := range []*cobra.Command{
showConfNode(),
showConfPeer(),
} {
cmd.AddCommand(subCmd)
}
return cmd
}
func showConfNode() *cobra.Command {
return &cobra.Command{
Use: "node",
Short: "Show the WireGuard configuration for a node in the Kilo network",
Long: "",
RunE: runShowConfNode,
Args: cobra.ExactArgs(1),
}
}
func showConfPeer() *cobra.Command {
return &cobra.Command{
Use: "peer",
Short: "Show the WireGuard configuration for a peer in the Kilo network",
Long: "",
RunE: runShowConfPeer,
Args: cobra.ExactArgs(1),
}
}
func runShowConfNode(_ *cobra.Command, args []string) error {
ns, err := opts.backend.Nodes().List()
if err != nil {
return fmt.Errorf("failed to list nodes: %v", err)
}
ps, err := opts.backend.Peers().List()
if err != nil {
return fmt.Errorf("failed to list peers: %v", err)
}
hostname := args[0]
nodes := make(map[string]*mesh.Node)
for _, n := range ns {
if n.Ready() {
nodes[n.Name] = n
}
}
if len(nodes) == 0 {
return errors.New("did not find any valid Kilo nodes in the cluster")
}
if _, ok := nodes[hostname]; !ok {
return fmt.Errorf("did not find any node named %q in the cluster", hostname)
}
peers := make(map[string]*mesh.Peer)
for _, p := range ps {
if p.Ready() {
peers[p.Name] = p
}
}
t, err := mesh.NewTopology(nodes, peers, opts.granularity, hostname, mesh.DefaultKiloPort, []byte{}, opts.subnet)
if err != nil {
return fmt.Errorf("failed to create topology: %v", err)
}
c, err := t.Conf().Bytes()
if err != nil {
return fmt.Errorf("failed to generate configuration: %v", err)
}
fmt.Printf(string(c))
return nil
}
func runShowConfPeer(_ *cobra.Command, args []string) error {
ns, err := opts.backend.Nodes().List()
if err != nil {
return fmt.Errorf("failed to list nodes: %v", err)
}
ps, err := opts.backend.Peers().List()
if err != nil {
return fmt.Errorf("failed to list peers: %v", err)
}
var hostname string
nodes := make(map[string]*mesh.Node)
for _, n := range ns {
if n.Ready() {
nodes[n.Name] = n
hostname = n.Name
}
}
if len(nodes) == 0 {
return errors.New("did not find any valid Kilo nodes in the cluster")
}
peer := args[0]
peers := make(map[string]*mesh.Peer)
for _, p := range ps {
if p.Ready() {
peers[p.Name] = p
}
}
if _, ok := peers[peer]; !ok {
return fmt.Errorf("did not find any peer named %q in the cluster", peer)
}
t, err := mesh.NewTopology(nodes, peers, opts.granularity, hostname, mesh.DefaultKiloPort, []byte{}, opts.subnet)
if err != nil {
return fmt.Errorf("failed to create topology: %v", err)
}
c, err := t.PeerConf(peer).Bytes()
if err != nil {
return fmt.Errorf("failed to generate configuration: %v", err)
}
fmt.Printf(string(c))
return nil
}

View File

@ -1,4 +1,5 @@
# Annotations
The following annotations can be added to any Kubernetes Node object to configure the Kilo network.
|Name|type|example|

View File

@ -17,6 +17,20 @@ rules:
- list
- patch
- watch
- apiGroups:
- kilo.squat.ai
resources:
- peers
verbs:
- list
- update
- watch
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding

View File

@ -17,6 +17,20 @@ rules:
- list
- patch
- watch
- apiGroups:
- kilo.squat.ai
resources:
- peers
verbs:
- list
- update
- watch
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding

View File

@ -17,6 +17,20 @@ rules:
- list
- patch
- watch
- apiGroups:
- kilo.squat.ai
resources:
- peers
verbs:
- list
- update
- watch
- apiGroups:
- apiextensions.k8s.io
resources:
- customresourcedefinitions
verbs:
- create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding

View File

@ -254,13 +254,17 @@ func EncapsulateRules(nodes []*net.IPNet) []Rule {
// ForwardRules returns a set of iptables rules that are necessary
// when traffic must be forwarded for the overlay.
func ForwardRules(subnet *net.IPNet) []Rule {
s := subnet.String()
return []Rule{
// Forward traffic to and from the overlay.
&rule{"filter", "FORWARD", []string{"-s", s, "-j", "ACCEPT"}, nil},
&rule{"filter", "FORWARD", []string{"-d", s, "-j", "ACCEPT"}, nil},
func ForwardRules(subnets ...*net.IPNet) []Rule {
var rules []Rule
for _, subnet := range subnets {
s := subnet.String()
rules = append(rules, []Rule{
// Forward traffic to and from the overlay.
&rule{"filter", "FORWARD", []string{"-s", s, "-j", "ACCEPT"}, nil},
&rule{"filter", "FORWARD", []string{"-d", s, "-j", "ACCEPT"}, nil},
}...)
}
return rules
}
// MasqueradeRules returns a set of iptables rules that are necessary

View File

@ -0,0 +1,20 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kilo
const (
// GroupName contains the API group name for Kilo API group.
GroupName = "kilo"
)

View File

@ -0,0 +1,19 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// +k8s:deepcopy-gen=package,register
// Package v1alpha1 is the v1alpha1 version of the API.
// +groupName=kilo
package v1alpha1

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
var (
// SchemeBuilder exposes an API scheme builder for this API version.
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
// AddToScheme exposes an AddToScheme func for this API version.
AddToScheme = SchemeBuilder.AddToScheme
)
// GroupName is the group name used in this package.
const GroupName = "kilo.squat.ai"
// SchemeGroupVersion is the group version used to register these objects.
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
// Resource takes an unqualified resource and returns a Group-qualified GroupResource.
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
// addKnownTypes adds the set of types defined in this package to the supplied scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Peer{},
&PeerList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}

View File

@ -0,0 +1,143 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1alpha1
import (
"encoding/json"
"errors"
"fmt"
"net"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
// PeerKind is the API kind for the peer resource.
PeerKind = "Peer"
// PeerPlural is the plural name for the peer resource.
PeerPlural = "peers"
)
// PeerShortNames are convenient shortnames for the peer resource.
var PeerShortNames = []string{"peer"}
// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:openapi-gen=true
// Peer is a WireGuard peer that should have access to the VPN.
type Peer struct {
metav1.TypeMeta `json:",inline"`
// Standard objects metadata. More info:
// https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#metadata
// +k8s:openapi-gen=false
metav1.ObjectMeta `json:"metadata"`
// Specification of the desired behavior of the Kilo Peer. More info:
// https://github.com/kubernetes/community/blob/master/contributors/devel/api-conventions.md#spec-and-status
Spec PeerSpec `json:"spec"`
}
// PeerSpec is the description and configuration of a peer.
// +k8s:openapi-gen=true
type PeerSpec struct {
// AllowedIPs is the list of IP addresses that are allowed
// for the given peer's tunnel.
AllowedIPs []string `json:"allowedIPs"`
// Endpoint is the initial endpoint for connections to the peer.
// +optional
Endpoint *PeerEndpoint `json:"endpoint"`
// PersistentKeepalive is the interval in seconds of the emission
// of keepalive packets to the peer. This defaults to 0, which
// disables the feature.
PersistentKeepalive int `json:"persistentKeepalive"`
// PublicKey is the WireGuard public key for the node.
PublicKey string `json:"publicKey"`
}
// PeerEndpoint represents a WireGuard enpoint, which is a ip:port tuple.
type PeerEndpoint struct {
// IP must be a valid IP address.
IP string `json:"ip"`
// Port must be a valid port number.
Port uint32 `json:"port"`
}
// PeerName is the peer resource's FQDN.
var PeerName = PeerPlural + "." + GroupName
// AsOwner creates a new owner reference for the peer to apply to dependent resource.
func (p *Peer) AsOwner() metav1.OwnerReference {
trueVar := true
return metav1.OwnerReference{
APIVersion: p.APIVersion,
Kind: p.Kind,
Name: p.Name,
UID: p.UID,
BlockOwnerDeletion: &trueVar,
Controller: &trueVar,
}
}
// Copy creates a deep copy of the peer.
func (p *Peer) Copy() *Peer {
new := Peer{}
b, err := json.Marshal(*p)
if err != nil {
panic(err)
}
err = json.Unmarshal(b, &new)
if err != nil {
panic(err)
}
return &new
}
// Validate ensures that all the fields of a peer's spec are valid.
func (p *Peer) Validate() error {
for _, ip := range p.Spec.AllowedIPs {
if _, n, err := net.ParseCIDR(ip); err != nil {
return fmt.Errorf("failed to parse %q as a valid IP address: %v", ip, err)
} else if n == nil {
return fmt.Errorf("got invalid IP address for %q", ip)
}
}
if p.Spec.Endpoint != nil {
if net.ParseIP(p.Spec.Endpoint.IP) == nil {
return fmt.Errorf("failed to parse %q as a valid IP address", p.Spec.Endpoint.IP)
}
if p.Spec.Endpoint.Port == 0 {
return fmt.Errorf("port must be a valid UDP port number, got %d", p.Spec.Endpoint.Port)
}
}
if p.Spec.PersistentKeepalive < 0 {
return fmt.Errorf("persistent keepalive must be greater than or equal to zero; got %q", p.Spec.PersistentKeepalive)
}
if len(p.Spec.PublicKey) == 0 {
return errors.New("public keys cannot be empty")
}
return nil
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// PeerList is a list of peers.
type PeerList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
// List of peers.
// More info: https://git.k8s.io/community/contributors/devel/api-conventions.md
Items []Peer `json:"items"`
}

View File

@ -0,0 +1,125 @@
// +build !ignore_autogenerated
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by deepcopy-gen. DO NOT EDIT.
package v1alpha1
import (
runtime "k8s.io/apimachinery/pkg/runtime"
)
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Peer) DeepCopyInto(out *Peer) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Peer.
func (in *Peer) DeepCopy() *Peer {
if in == nil {
return nil
}
out := new(Peer)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Peer) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PeerEndpoint) DeepCopyInto(out *PeerEndpoint) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PeerEndpoint.
func (in *PeerEndpoint) DeepCopy() *PeerEndpoint {
if in == nil {
return nil
}
out := new(PeerEndpoint)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PeerList) DeepCopyInto(out *PeerList) {
*out = *in
out.TypeMeta = in.TypeMeta
out.ListMeta = in.ListMeta
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Peer, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PeerList.
func (in *PeerList) DeepCopy() *PeerList {
if in == nil {
return nil
}
out := new(PeerList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PeerList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PeerSpec) DeepCopyInto(out *PeerSpec) {
*out = *in
if in.AllowedIPs != nil {
in, out := &in.AllowedIPs, &out.AllowedIPs
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.Endpoint != nil {
in, out := &in.Endpoint, &out.Endpoint
*out = new(PeerEndpoint)
**out = **in
}
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PeerSpec.
func (in *PeerSpec) DeepCopy() *PeerSpec {
if in == nil {
return nil
}
out := new(PeerSpec)
in.DeepCopyInto(out)
return out
}

View File

@ -24,7 +24,11 @@ import (
"strings"
"time"
crdutils "github.com/ant31/crd-validation/pkg"
v1 "k8s.io/api/core/v1"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
apiextensions "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/strategicpatch"
@ -33,7 +37,12 @@ import (
v1listers "k8s.io/client-go/listers/core/v1"
"k8s.io/client-go/tools/cache"
"github.com/squat/kilo/pkg/k8s/apis/kilo/v1alpha1"
kiloclient "github.com/squat/kilo/pkg/k8s/clientset/versioned"
v1alpha1informers "github.com/squat/kilo/pkg/k8s/informers/kilo/v1alpha1"
v1alpha1listers "github.com/squat/kilo/pkg/k8s/listers/kilo/v1alpha1"
"github.com/squat/kilo/pkg/mesh"
"github.com/squat/kilo/pkg/wireguard"
)
const (
@ -52,43 +61,74 @@ const (
)
type backend struct {
nodes *nodeBackend
peers *peerBackend
}
// Nodes implements the mesh.Backend interface.
func (b *backend) Nodes() mesh.NodeBackend {
return b.nodes
}
// Peers implements the mesh.Backend interface.
func (b *backend) Peers() mesh.PeerBackend {
return b.peers
}
type nodeBackend struct {
client kubernetes.Interface
events chan *mesh.Event
events chan *mesh.NodeEvent
informer cache.SharedIndexInformer
lister v1listers.NodeLister
}
type peerBackend struct {
client kiloclient.Interface
extensionsClient apiextensions.Interface
events chan *mesh.PeerEvent
informer cache.SharedIndexInformer
lister v1alpha1listers.PeerLister
}
// New creates a new instance of a mesh.Backend.
func New(client kubernetes.Interface) mesh.Backend {
informer := v1informers.NewNodeInformer(client, 5*time.Minute, nil)
func New(c kubernetes.Interface, kc kiloclient.Interface, ec apiextensions.Interface) mesh.Backend {
ni := v1informers.NewNodeInformer(c, 5*time.Minute, nil)
pi := v1alpha1informers.NewPeerInformer(kc, 5*time.Minute, nil)
b := &backend{
client: client,
events: make(chan *mesh.Event),
informer: informer,
lister: v1listers.NewNodeLister(informer.GetIndexer()),
return &backend{
&nodeBackend{
client: c,
events: make(chan *mesh.NodeEvent),
informer: ni,
lister: v1listers.NewNodeLister(ni.GetIndexer()),
},
&peerBackend{
client: kc,
extensionsClient: ec,
events: make(chan *mesh.PeerEvent),
informer: pi,
lister: v1alpha1listers.NewPeerLister(pi.GetIndexer()),
},
}
return b
}
// CleanUp removes configuration applied to the backend.
func (b *backend) CleanUp(name string) error {
func (nb *nodeBackend) CleanUp(name string) error {
patch := []byte("[" + strings.Join([]string{
fmt.Sprintf(jsonRemovePatch, path.Join("/metadata", "annotations", strings.Replace(externalIPAnnotationKey, "/", jsonPatchSlash, 1))),
fmt.Sprintf(jsonRemovePatch, path.Join("/metadata", "annotations", strings.Replace(internalIPAnnotationKey, "/", jsonPatchSlash, 1))),
fmt.Sprintf(jsonRemovePatch, path.Join("/metadata", "annotations", strings.Replace(keyAnnotationKey, "/", jsonPatchSlash, 1))),
fmt.Sprintf(jsonRemovePatch, path.Join("/metadata", "annotations", strings.Replace(lastSeenAnnotationKey, "/", jsonPatchSlash, 1))),
}, ",") + "]")
if _, err := b.client.CoreV1().Nodes().Patch(name, types.JSONPatchType, patch); err != nil {
if _, err := nb.client.CoreV1().Nodes().Patch(name, types.JSONPatchType, patch); err != nil {
return fmt.Errorf("failed to patch node: %v", err)
}
return nil
}
// Get gets a single Node by name.
func (b *backend) Get(name string) (*mesh.Node, error) {
n, err := b.lister.Get(name)
func (nb *nodeBackend) Get(name string) (*mesh.Node, error) {
n, err := nb.lister.Get(name)
if err != nil {
return nil, err
}
@ -97,14 +137,14 @@ func (b *backend) Get(name string) (*mesh.Node, error) {
// Init initializes the backend; for this backend that means
// syncing the informer cache.
func (b *backend) Init(stop <-chan struct{}) error {
go b.informer.Run(stop)
func (nb *nodeBackend) Init(stop <-chan struct{}) error {
go nb.informer.Run(stop)
if ok := cache.WaitForCacheSync(stop, func() bool {
return b.informer.HasSynced()
return nb.informer.HasSynced()
}); !ok {
return errors.New("failed to start sync node cache")
}
b.informer.AddEventHandler(
nb.informer.AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
n, ok := obj.(*v1.Node)
@ -112,7 +152,7 @@ func (b *backend) Init(stop <-chan struct{}) error {
// Failed to decode Node; ignoring...
return
}
b.events <- &mesh.Event{Type: mesh.AddEvent, Node: translateNode(n)}
nb.events <- &mesh.NodeEvent{Type: mesh.AddEvent, Node: translateNode(n)}
},
UpdateFunc: func(_, obj interface{}) {
n, ok := obj.(*v1.Node)
@ -120,7 +160,7 @@ func (b *backend) Init(stop <-chan struct{}) error {
// Failed to decode Node; ignoring...
return
}
b.events <- &mesh.Event{Type: mesh.UpdateEvent, Node: translateNode(n)}
nb.events <- &mesh.NodeEvent{Type: mesh.UpdateEvent, Node: translateNode(n)}
},
DeleteFunc: func(obj interface{}) {
n, ok := obj.(*v1.Node)
@ -128,7 +168,7 @@ func (b *backend) Init(stop <-chan struct{}) error {
// Failed to decode Node; ignoring...
return
}
b.events <- &mesh.Event{Type: mesh.DeleteEvent, Node: translateNode(n)}
nb.events <- &mesh.NodeEvent{Type: mesh.DeleteEvent, Node: translateNode(n)}
},
},
)
@ -136,8 +176,8 @@ func (b *backend) Init(stop <-chan struct{}) error {
}
// List gets all the Nodes in the cluster.
func (b *backend) List() ([]*mesh.Node, error) {
ns, err := b.lister.List(labels.Everything())
func (nb *nodeBackend) List() ([]*mesh.Node, error) {
ns, err := nb.lister.List(labels.Everything())
if err != nil {
return nil, err
}
@ -149,8 +189,8 @@ func (b *backend) List() ([]*mesh.Node, error) {
}
// Set sets the fields of a node.
func (b *backend) Set(name string, node *mesh.Node) error {
old, err := b.lister.Get(name)
func (nb *nodeBackend) Set(name string, node *mesh.Node) error {
old, err := nb.lister.Get(name)
if err != nil {
return fmt.Errorf("failed to find node: %v", err)
}
@ -171,15 +211,15 @@ func (b *backend) Set(name string, node *mesh.Node) error {
if err != nil {
return fmt.Errorf("failed to create patch for node %q: %v", n.Name, err)
}
if _, err = b.client.CoreV1().Nodes().Patch(name, types.StrategicMergePatchType, patch); err != nil {
if _, err = nb.client.CoreV1().Nodes().Patch(name, types.StrategicMergePatchType, patch); err != nil {
return fmt.Errorf("failed to patch node: %v", err)
}
return nil
}
// Watch returns a chan of node events.
func (b *backend) Watch() <-chan *mesh.Event {
return b.events
func (nb *nodeBackend) Watch() <-chan *mesh.NodeEvent {
return nb.events
}
// translateNode translates a Kubernetes Node to a mesh.Node.
@ -214,7 +254,7 @@ func translateNode(node *v1.Node) *mesh.Node {
}
return &mesh.Node{
// ExternalIP and InternalIP should only ever fail to parse if the
// remote node's mesh has not yet set its IP address;
// remote node's agent has not yet set its IP address;
// in this case the IP will be nil and
// the mesh can wait for the node to be updated.
ExternalIP: normalizeIP(externalIP),
@ -228,10 +268,179 @@ func translateNode(node *v1.Node) *mesh.Node {
}
}
// translatePeer translates a Peer CRD to a mesh.Peer.
func translatePeer(peer *v1alpha1.Peer) *mesh.Peer {
if peer == nil {
return nil
}
var aips []*net.IPNet
for _, aip := range peer.Spec.AllowedIPs {
aip := normalizeIP(aip)
// Skip any invalid IPs.
if aip == nil {
continue
}
aips = append(aips, aip)
}
var endpoint *wireguard.Endpoint
if peer.Spec.Endpoint != nil {
ip := net.ParseIP(peer.Spec.Endpoint.IP)
if ip4 := ip.To4(); ip4 != nil {
ip = ip4
} else {
ip = ip.To16()
}
if peer.Spec.Endpoint.Port > 0 && ip != nil {
endpoint = &wireguard.Endpoint{
IP: ip,
Port: peer.Spec.Endpoint.Port,
}
}
}
var key []byte
if len(peer.Spec.PublicKey) > 0 {
key = []byte(peer.Spec.PublicKey)
}
var pka int
if peer.Spec.PersistentKeepalive > 0 {
pka = peer.Spec.PersistentKeepalive
}
return &mesh.Peer{
Name: peer.Name,
Peer: wireguard.Peer{
AllowedIPs: aips,
Endpoint: endpoint,
PublicKey: key,
PersistentKeepalive: pka,
},
}
}
// CleanUp removes configuration applied to the backend.
func (pb *peerBackend) CleanUp(name string) error {
return nil
}
// Get gets a single Peer by name.
func (pb *peerBackend) Get(name string) (*mesh.Peer, error) {
p, err := pb.lister.Get(name)
if err != nil {
return nil, err
}
return translatePeer(p), nil
}
// Init initializes the backend; for this backend that means
// syncing the informer cache.
func (pb *peerBackend) Init(stop <-chan struct{}) error {
// Register CRD.
crd := crdutils.NewCustomResourceDefinition(crdutils.Config{
SpecDefinitionName: "github.com/squat/kilo/pkg/k8s/apis/kilo/v1alpha1.Peer",
EnableValidation: true,
ResourceScope: string(v1beta1.ClusterScoped),
Group: v1alpha1.GroupName,
Kind: v1alpha1.PeerKind,
Version: v1alpha1.SchemeGroupVersion.Version,
Plural: v1alpha1.PeerPlural,
ShortNames: v1alpha1.PeerShortNames,
GetOpenAPIDefinitions: v1alpha1.GetOpenAPIDefinitions,
})
crd.Spec.Subresources.Scale = nil
crd.Spec.Subresources.Status = nil
_, err := pb.extensionsClient.ApiextensionsV1beta1().CustomResourceDefinitions().Create(crd)
if err != nil && !apierrors.IsAlreadyExists(err) {
return fmt.Errorf("failed to create CRD: %v", err)
}
go pb.informer.Run(stop)
if ok := cache.WaitForCacheSync(stop, func() bool {
return pb.informer.HasSynced()
}); !ok {
return errors.New("failed to start sync peer cache")
}
pb.informer.AddEventHandler(
cache.ResourceEventHandlerFuncs{
AddFunc: func(obj interface{}) {
p, ok := obj.(*v1alpha1.Peer)
if !ok || p.Validate() != nil {
// Failed to decode Peer; ignoring...
return
}
pb.events <- &mesh.PeerEvent{Type: mesh.AddEvent, Peer: translatePeer(p)}
},
UpdateFunc: func(_, obj interface{}) {
p, ok := obj.(*v1alpha1.Peer)
if !ok || p.Validate() != nil {
// Failed to decode Peer; ignoring...
return
}
pb.events <- &mesh.PeerEvent{Type: mesh.UpdateEvent, Peer: translatePeer(p)}
},
DeleteFunc: func(obj interface{}) {
p, ok := obj.(*v1alpha1.Peer)
if !ok || p.Validate() != nil {
// Failed to decode Peer; ignoring...
return
}
pb.events <- &mesh.PeerEvent{Type: mesh.DeleteEvent, Peer: translatePeer(p)}
},
},
)
return nil
}
// List gets all the Peers in the cluster.
func (pb *peerBackend) List() ([]*mesh.Peer, error) {
ps, err := pb.lister.List(labels.Everything())
if err != nil {
return nil, err
}
peers := make([]*mesh.Peer, len(ps))
for i := range ps {
// Skip invalid peers.
if ps[i].Validate() != nil {
continue
}
peers[i] = translatePeer(ps[i])
}
return peers, nil
}
// Set sets the fields of a peer.
func (pb *peerBackend) Set(name string, peer *mesh.Peer) error {
old, err := pb.lister.Get(name)
if err != nil {
return fmt.Errorf("failed to find peer: %v", err)
}
p := old.DeepCopy()
p.Spec.AllowedIPs = make([]string, len(peer.AllowedIPs))
for i := range peer.AllowedIPs {
p.Spec.AllowedIPs[i] = peer.AllowedIPs[i].String()
}
if peer.Endpoint != nil {
p.Spec.Endpoint = &v1alpha1.PeerEndpoint{
IP: peer.Endpoint.IP.String(),
Port: peer.Endpoint.Port,
}
}
p.Spec.PersistentKeepalive = peer.PersistentKeepalive
p.Spec.PublicKey = string(peer.PublicKey)
if _, err = pb.client.KiloV1alpha1().Peers().Update(p); err != nil {
return fmt.Errorf("failed to update peer: %v", err)
}
return nil
}
// Watch returns a chan of peer events.
func (pb *peerBackend) Watch() <-chan *mesh.PeerEvent {
return pb.events
}
func normalizeIP(ip string) *net.IPNet {
i, ipNet, _ := net.ParseCIDR(ip)
if ipNet == nil {
return ipNet
i, ipNet, err := net.ParseCIDR(ip)
if err != nil || ipNet == nil {
return nil
}
if ip4 := i.To4(); ip4 != nil {
ipNet.IP = ip4

View File

@ -21,7 +21,9 @@ import (
"github.com/kylelemons/godebug/pretty"
v1 "k8s.io/api/core/v1"
"github.com/squat/kilo/pkg/k8s/apis/kilo/v1alpha1"
"github.com/squat/kilo/pkg/mesh"
"github.com/squat/kilo/pkg/wireguard"
)
func TestTranslateNode(t *testing.T) {
@ -152,3 +154,113 @@ func TestTranslateNode(t *testing.T) {
}
}
}
func TestTranslatePeer(t *testing.T) {
for _, tc := range []struct {
name string
out *mesh.Peer
spec v1alpha1.PeerSpec
}{
{
name: "empty",
out: &mesh.Peer{},
},
{
name: "invalid ips",
spec: v1alpha1.PeerSpec{
AllowedIPs: []string{
"10.0.0.1",
"foo",
},
},
out: &mesh.Peer{},
},
{
name: "valid ips",
spec: v1alpha1.PeerSpec{
AllowedIPs: []string{
"10.0.0.1/24",
"10.0.0.2/32",
},
},
out: &mesh.Peer{
Peer: wireguard.Peer{
AllowedIPs: []*net.IPNet{
{IP: net.ParseIP("10.0.0.1"), Mask: net.CIDRMask(24, 32)},
{IP: net.ParseIP("10.0.0.2"), Mask: net.CIDRMask(32, 32)},
},
},
},
},
{
name: "invalid endpoint ip",
spec: v1alpha1.PeerSpec{
Endpoint: &v1alpha1.PeerEndpoint{
IP: "foo",
Port: mesh.DefaultKiloPort,
},
},
out: &mesh.Peer{},
},
{
name: "valid endpoint",
spec: v1alpha1.PeerSpec{
Endpoint: &v1alpha1.PeerEndpoint{
IP: "10.0.0.1",
Port: mesh.DefaultKiloPort,
},
},
out: &mesh.Peer{
Peer: wireguard.Peer{
Endpoint: &wireguard.Endpoint{
IP: net.ParseIP("10.0.0.1"),
Port: mesh.DefaultKiloPort,
},
},
},
},
{
name: "empty key",
spec: v1alpha1.PeerSpec{
PublicKey: "",
},
out: &mesh.Peer{},
},
{
name: "valid key",
spec: v1alpha1.PeerSpec{
PublicKey: "foo",
},
out: &mesh.Peer{
Peer: wireguard.Peer{
PublicKey: []byte("foo"),
},
},
},
{
name: "invalid keepalive",
spec: v1alpha1.PeerSpec{
PersistentKeepalive: -1,
},
out: &mesh.Peer{},
},
{
name: "valid keepalive",
spec: v1alpha1.PeerSpec{
PersistentKeepalive: 1,
},
out: &mesh.Peer{
Peer: wireguard.Peer{
PersistentKeepalive: 1,
},
},
},
} {
p := &v1alpha1.Peer{}
p.Spec = tc.spec
peer := translatePeer(p)
if diff := pretty.Compare(peer, tc.out); diff != "" {
t.Errorf("test case %q: got diff: %v", tc.name, diff)
}
}
}

View File

@ -0,0 +1,88 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
package versioned
import (
kilov1alpha1 "github.com/squat/kilo/pkg/k8s/clientset/versioned/typed/kilo/v1alpha1"
discovery "k8s.io/client-go/discovery"
rest "k8s.io/client-go/rest"
flowcontrol "k8s.io/client-go/util/flowcontrol"
)
type Interface interface {
Discovery() discovery.DiscoveryInterface
KiloV1alpha1() kilov1alpha1.KiloV1alpha1Interface
}
// Clientset contains the clients for groups. Each group has exactly one
// version included in a Clientset.
type Clientset struct {
*discovery.DiscoveryClient
kiloV1alpha1 *kilov1alpha1.KiloV1alpha1Client
}
// KiloV1alpha1 retrieves the KiloV1alpha1Client
func (c *Clientset) KiloV1alpha1() kilov1alpha1.KiloV1alpha1Interface {
return c.kiloV1alpha1
}
// Discovery retrieves the DiscoveryClient
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
if c == nil {
return nil
}
return c.DiscoveryClient
}
// NewForConfig creates a new Clientset for the given config.
func NewForConfig(c *rest.Config) (*Clientset, error) {
configShallowCopy := *c
if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 {
configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst)
}
var cs Clientset
var err error
cs.kiloV1alpha1, err = kilov1alpha1.NewForConfig(&configShallowCopy)
if err != nil {
return nil, err
}
cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(&configShallowCopy)
if err != nil {
return nil, err
}
return &cs, nil
}
// NewForConfigOrDie creates a new Clientset for the given config and
// panics if there is an error in the config.
func NewForConfigOrDie(c *rest.Config) *Clientset {
var cs Clientset
cs.kiloV1alpha1 = kilov1alpha1.NewForConfigOrDie(c)
cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c)
return &cs
}
// New creates a new Clientset for the given RESTClient.
func New(c rest.Interface) *Clientset {
var cs Clientset
cs.kiloV1alpha1 = kilov1alpha1.New(c)
cs.DiscoveryClient = discovery.NewDiscoveryClient(c)
return &cs
}

View File

@ -0,0 +1,18 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
// This package has the automatically generated clientset.
package versioned

View File

@ -0,0 +1,75 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
clientset "github.com/squat/kilo/pkg/k8s/clientset/versioned"
kilov1alpha1 "github.com/squat/kilo/pkg/k8s/clientset/versioned/typed/kilo/v1alpha1"
fakekilov1alpha1 "github.com/squat/kilo/pkg/k8s/clientset/versioned/typed/kilo/v1alpha1/fake"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/discovery"
fakediscovery "k8s.io/client-go/discovery/fake"
"k8s.io/client-go/testing"
)
// NewSimpleClientset returns a clientset that will respond with the provided objects.
// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,
// without applying any validations and/or defaults. It shouldn't be considered a replacement
// for a real clientset and is mostly useful in simple unit tests.
func NewSimpleClientset(objects ...runtime.Object) *Clientset {
o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())
for _, obj := range objects {
if err := o.Add(obj); err != nil {
panic(err)
}
}
cs := &Clientset{}
cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake}
cs.AddReactor("*", "*", testing.ObjectReaction(o))
cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) {
gvr := action.GetResource()
ns := action.GetNamespace()
watch, err := o.Watch(gvr, ns)
if err != nil {
return false, nil, err
}
return true, watch, nil
})
return cs
}
// Clientset implements clientset.Interface. Meant to be embedded into a
// struct to get a default implementation. This makes faking out just the method
// you want to test easier.
type Clientset struct {
testing.Fake
discovery *fakediscovery.FakeDiscovery
}
func (c *Clientset) Discovery() discovery.DiscoveryInterface {
return c.discovery
}
var _ clientset.Interface = &Clientset{}
// KiloV1alpha1 retrieves the KiloV1alpha1Client
func (c *Clientset) KiloV1alpha1() kilov1alpha1.KiloV1alpha1Interface {
return &fakekilov1alpha1.FakeKiloV1alpha1{Fake: &c.Fake}
}

View File

@ -0,0 +1,18 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
// This package has the automatically generated fake clientset.
package fake

View File

@ -0,0 +1,54 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
kilov1alpha1 "github.com/squat/kilo/pkg/k8s/apis/kilo/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)
var scheme = runtime.NewScheme()
var codecs = serializer.NewCodecFactory(scheme)
var parameterCodec = runtime.NewParameterCodec(scheme)
var localSchemeBuilder = runtime.SchemeBuilder{
kilov1alpha1.AddToScheme,
}
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in:
//
// import (
// "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kubernetes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// )
//
// kclientset, _ := kubernetes.NewForConfig(c)
// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
//
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly.
var AddToScheme = localSchemeBuilder.AddToScheme
func init() {
v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"})
utilruntime.Must(AddToScheme(scheme))
}

View File

@ -0,0 +1,18 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
// This package contains the scheme of the automatically generated clientset.
package scheme

View File

@ -0,0 +1,54 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
package scheme
import (
kilov1alpha1 "github.com/squat/kilo/pkg/k8s/apis/kilo/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)
var Scheme = runtime.NewScheme()
var Codecs = serializer.NewCodecFactory(Scheme)
var ParameterCodec = runtime.NewParameterCodec(Scheme)
var localSchemeBuilder = runtime.SchemeBuilder{
kilov1alpha1.AddToScheme,
}
// AddToScheme adds all types of this clientset into the given scheme. This allows composition
// of clientsets, like in:
//
// import (
// "k8s.io/client-go/kubernetes"
// clientsetscheme "k8s.io/client-go/kubernetes/scheme"
// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme"
// )
//
// kclientset, _ := kubernetes.NewForConfig(c)
// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme)
//
// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types
// correctly.
var AddToScheme = localSchemeBuilder.AddToScheme
func init() {
v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
utilruntime.Must(AddToScheme(Scheme))
}

View File

@ -0,0 +1,18 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
// This package has the automatically generated typed clients.
package v1alpha1

View File

@ -0,0 +1,18 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
// Package fake has the automatically generated clients.
package fake

View File

@ -0,0 +1,38 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
v1alpha1 "github.com/squat/kilo/pkg/k8s/clientset/versioned/typed/kilo/v1alpha1"
rest "k8s.io/client-go/rest"
testing "k8s.io/client-go/testing"
)
type FakeKiloV1alpha1 struct {
*testing.Fake
}
func (c *FakeKiloV1alpha1) Peers() v1alpha1.PeerInterface {
return &FakePeers{c}
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *FakeKiloV1alpha1) RESTClient() rest.Interface {
var ret *rest.RESTClient
return ret
}

View File

@ -0,0 +1,118 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
package fake
import (
v1alpha1 "github.com/squat/kilo/pkg/k8s/apis/kilo/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
labels "k8s.io/apimachinery/pkg/labels"
schema "k8s.io/apimachinery/pkg/runtime/schema"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
testing "k8s.io/client-go/testing"
)
// FakePeers implements PeerInterface
type FakePeers struct {
Fake *FakeKiloV1alpha1
}
var peersResource = schema.GroupVersionResource{Group: "kilo", Version: "v1alpha1", Resource: "peers"}
var peersKind = schema.GroupVersionKind{Group: "kilo", Version: "v1alpha1", Kind: "Peer"}
// Get takes name of the peer, and returns the corresponding peer object, and an error if there is any.
func (c *FakePeers) Get(name string, options v1.GetOptions) (result *v1alpha1.Peer, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootGetAction(peersResource, name), &v1alpha1.Peer{})
if obj == nil {
return nil, err
}
return obj.(*v1alpha1.Peer), err
}
// List takes label and field selectors, and returns the list of Peers that match those selectors.
func (c *FakePeers) List(opts v1.ListOptions) (result *v1alpha1.PeerList, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootListAction(peersResource, peersKind, opts), &v1alpha1.PeerList{})
if obj == nil {
return nil, err
}
label, _, _ := testing.ExtractFromListOptions(opts)
if label == nil {
label = labels.Everything()
}
list := &v1alpha1.PeerList{ListMeta: obj.(*v1alpha1.PeerList).ListMeta}
for _, item := range obj.(*v1alpha1.PeerList).Items {
if label.Matches(labels.Set(item.Labels)) {
list.Items = append(list.Items, item)
}
}
return list, err
}
// Watch returns a watch.Interface that watches the requested peers.
func (c *FakePeers) Watch(opts v1.ListOptions) (watch.Interface, error) {
return c.Fake.
InvokesWatch(testing.NewRootWatchAction(peersResource, opts))
}
// Create takes the representation of a peer and creates it. Returns the server's representation of the peer, and an error, if there is any.
func (c *FakePeers) Create(peer *v1alpha1.Peer) (result *v1alpha1.Peer, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootCreateAction(peersResource, peer), &v1alpha1.Peer{})
if obj == nil {
return nil, err
}
return obj.(*v1alpha1.Peer), err
}
// Update takes the representation of a peer and updates it. Returns the server's representation of the peer, and an error, if there is any.
func (c *FakePeers) Update(peer *v1alpha1.Peer) (result *v1alpha1.Peer, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootUpdateAction(peersResource, peer), &v1alpha1.Peer{})
if obj == nil {
return nil, err
}
return obj.(*v1alpha1.Peer), err
}
// Delete takes name of the peer and deletes it. Returns an error if one occurs.
func (c *FakePeers) Delete(name string, options *v1.DeleteOptions) error {
_, err := c.Fake.
Invokes(testing.NewRootDeleteAction(peersResource, name), &v1alpha1.Peer{})
return err
}
// DeleteCollection deletes a collection of objects.
func (c *FakePeers) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
action := testing.NewRootDeleteCollectionAction(peersResource, listOptions)
_, err := c.Fake.Invokes(action, &v1alpha1.PeerList{})
return err
}
// Patch applies the patch and returns the patched peer.
func (c *FakePeers) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Peer, err error) {
obj, err := c.Fake.
Invokes(testing.NewRootPatchSubresourceAction(peersResource, name, pt, data, subresources...), &v1alpha1.Peer{})
if obj == nil {
return nil, err
}
return obj.(*v1alpha1.Peer), err
}

View File

@ -0,0 +1,19 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
package v1alpha1
type PeerExpansion interface{}

View File

@ -0,0 +1,88 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
package v1alpha1
import (
v1alpha1 "github.com/squat/kilo/pkg/k8s/apis/kilo/v1alpha1"
"github.com/squat/kilo/pkg/k8s/clientset/versioned/scheme"
serializer "k8s.io/apimachinery/pkg/runtime/serializer"
rest "k8s.io/client-go/rest"
)
type KiloV1alpha1Interface interface {
RESTClient() rest.Interface
PeersGetter
}
// KiloV1alpha1Client is used to interact with features provided by the kilo group.
type KiloV1alpha1Client struct {
restClient rest.Interface
}
func (c *KiloV1alpha1Client) Peers() PeerInterface {
return newPeers(c)
}
// NewForConfig creates a new KiloV1alpha1Client for the given config.
func NewForConfig(c *rest.Config) (*KiloV1alpha1Client, error) {
config := *c
if err := setConfigDefaults(&config); err != nil {
return nil, err
}
client, err := rest.RESTClientFor(&config)
if err != nil {
return nil, err
}
return &KiloV1alpha1Client{client}, nil
}
// NewForConfigOrDie creates a new KiloV1alpha1Client for the given config and
// panics if there is an error in the config.
func NewForConfigOrDie(c *rest.Config) *KiloV1alpha1Client {
client, err := NewForConfig(c)
if err != nil {
panic(err)
}
return client
}
// New creates a new KiloV1alpha1Client for the given RESTClient.
func New(c rest.Interface) *KiloV1alpha1Client {
return &KiloV1alpha1Client{c}
}
func setConfigDefaults(config *rest.Config) error {
gv := v1alpha1.SchemeGroupVersion
config.GroupVersion = &gv
config.APIPath = "/apis"
config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs}
if config.UserAgent == "" {
config.UserAgent = rest.DefaultKubernetesUserAgent()
}
return nil
}
// RESTClient returns a RESTClient that is used to communicate
// with API server by this client implementation.
func (c *KiloV1alpha1Client) RESTClient() rest.Interface {
if c == nil {
return nil
}
return c.restClient
}

View File

@ -0,0 +1,162 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by client-gen. DO NOT EDIT.
package v1alpha1
import (
"time"
v1alpha1 "github.com/squat/kilo/pkg/k8s/apis/kilo/v1alpha1"
scheme "github.com/squat/kilo/pkg/k8s/clientset/versioned/scheme"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
types "k8s.io/apimachinery/pkg/types"
watch "k8s.io/apimachinery/pkg/watch"
rest "k8s.io/client-go/rest"
)
// PeersGetter has a method to return a PeerInterface.
// A group's client should implement this interface.
type PeersGetter interface {
Peers() PeerInterface
}
// PeerInterface has methods to work with Peer resources.
type PeerInterface interface {
Create(*v1alpha1.Peer) (*v1alpha1.Peer, error)
Update(*v1alpha1.Peer) (*v1alpha1.Peer, error)
Delete(name string, options *v1.DeleteOptions) error
DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error
Get(name string, options v1.GetOptions) (*v1alpha1.Peer, error)
List(opts v1.ListOptions) (*v1alpha1.PeerList, error)
Watch(opts v1.ListOptions) (watch.Interface, error)
Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Peer, err error)
PeerExpansion
}
// peers implements PeerInterface
type peers struct {
client rest.Interface
}
// newPeers returns a Peers
func newPeers(c *KiloV1alpha1Client) *peers {
return &peers{
client: c.RESTClient(),
}
}
// Get takes name of the peer, and returns the corresponding peer object, and an error if there is any.
func (c *peers) Get(name string, options v1.GetOptions) (result *v1alpha1.Peer, err error) {
result = &v1alpha1.Peer{}
err = c.client.Get().
Resource("peers").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Do().
Into(result)
return
}
// List takes label and field selectors, and returns the list of Peers that match those selectors.
func (c *peers) List(opts v1.ListOptions) (result *v1alpha1.PeerList, err error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
result = &v1alpha1.PeerList{}
err = c.client.Get().
Resource("peers").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Do().
Into(result)
return
}
// Watch returns a watch.Interface that watches the requested peers.
func (c *peers) Watch(opts v1.ListOptions) (watch.Interface, error) {
var timeout time.Duration
if opts.TimeoutSeconds != nil {
timeout = time.Duration(*opts.TimeoutSeconds) * time.Second
}
opts.Watch = true
return c.client.Get().
Resource("peers").
VersionedParams(&opts, scheme.ParameterCodec).
Timeout(timeout).
Watch()
}
// Create takes the representation of a peer and creates it. Returns the server's representation of the peer, and an error, if there is any.
func (c *peers) Create(peer *v1alpha1.Peer) (result *v1alpha1.Peer, err error) {
result = &v1alpha1.Peer{}
err = c.client.Post().
Resource("peers").
Body(peer).
Do().
Into(result)
return
}
// Update takes the representation of a peer and updates it. Returns the server's representation of the peer, and an error, if there is any.
func (c *peers) Update(peer *v1alpha1.Peer) (result *v1alpha1.Peer, err error) {
result = &v1alpha1.Peer{}
err = c.client.Put().
Resource("peers").
Name(peer.Name).
Body(peer).
Do().
Into(result)
return
}
// Delete takes name of the peer and deletes it. Returns an error if one occurs.
func (c *peers) Delete(name string, options *v1.DeleteOptions) error {
return c.client.Delete().
Resource("peers").
Name(name).
Body(options).
Do().
Error()
}
// DeleteCollection deletes a collection of objects.
func (c *peers) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error {
var timeout time.Duration
if listOptions.TimeoutSeconds != nil {
timeout = time.Duration(*listOptions.TimeoutSeconds) * time.Second
}
return c.client.Delete().
Resource("peers").
VersionedParams(&listOptions, scheme.ParameterCodec).
Timeout(timeout).
Body(options).
Do().
Error()
}
// Patch applies the patch and returns the patched peer.
func (c *peers) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Peer, err error) {
result = &v1alpha1.Peer{}
err = c.client.Patch(pt).
Resource("peers").
SubResource(subresources...).
Name(name).
Body(data).
Do().
Into(result)
return
}

View File

@ -0,0 +1,178 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by informer-gen. DO NOT EDIT.
package informers
import (
reflect "reflect"
sync "sync"
time "time"
versioned "github.com/squat/kilo/pkg/k8s/clientset/versioned"
internalinterfaces "github.com/squat/kilo/pkg/k8s/informers/internalinterfaces"
kilo "github.com/squat/kilo/pkg/k8s/informers/kilo"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
cache "k8s.io/client-go/tools/cache"
)
// SharedInformerOption defines the functional option type for SharedInformerFactory.
type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory
type sharedInformerFactory struct {
client versioned.Interface
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
lock sync.Mutex
defaultResync time.Duration
customResync map[reflect.Type]time.Duration
informers map[reflect.Type]cache.SharedIndexInformer
// startedInformers is used for tracking which informers have been started.
// This allows Start() to be called multiple times safely.
startedInformers map[reflect.Type]bool
}
// WithCustomResyncConfig sets a custom resync period for the specified informer types.
func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
for k, v := range resyncConfig {
factory.customResync[reflect.TypeOf(k)] = v
}
return factory
}
}
// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory.
func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.tweakListOptions = tweakListOptions
return factory
}
}
// WithNamespace limits the SharedInformerFactory to the specified namespace.
func WithNamespace(namespace string) SharedInformerOption {
return func(factory *sharedInformerFactory) *sharedInformerFactory {
factory.namespace = namespace
return factory
}
}
// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces.
func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory {
return NewSharedInformerFactoryWithOptions(client, defaultResync)
}
// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory.
// Listers obtained via this SharedInformerFactory will be subject to the same filters
// as specified here.
// Deprecated: Please use NewSharedInformerFactoryWithOptions instead
func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory {
return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions))
}
// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options.
func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory {
factory := &sharedInformerFactory{
client: client,
namespace: v1.NamespaceAll,
defaultResync: defaultResync,
informers: make(map[reflect.Type]cache.SharedIndexInformer),
startedInformers: make(map[reflect.Type]bool),
customResync: make(map[reflect.Type]time.Duration),
}
// Apply all options
for _, opt := range options {
factory = opt(factory)
}
return factory
}
// Start initializes all requested informers.
func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) {
f.lock.Lock()
defer f.lock.Unlock()
for informerType, informer := range f.informers {
if !f.startedInformers[informerType] {
go informer.Run(stopCh)
f.startedInformers[informerType] = true
}
}
}
// WaitForCacheSync waits for all started informers' cache were synced.
func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool {
informers := func() map[reflect.Type]cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informers := map[reflect.Type]cache.SharedIndexInformer{}
for informerType, informer := range f.informers {
if f.startedInformers[informerType] {
informers[informerType] = informer
}
}
return informers
}()
res := map[reflect.Type]bool{}
for informType, informer := range informers {
res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced)
}
return res
}
// InternalInformerFor returns the SharedIndexInformer for obj using an internal
// client.
func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer {
f.lock.Lock()
defer f.lock.Unlock()
informerType := reflect.TypeOf(obj)
informer, exists := f.informers[informerType]
if exists {
return informer
}
resyncPeriod, exists := f.customResync[informerType]
if !exists {
resyncPeriod = f.defaultResync
}
informer = newFunc(f.client, resyncPeriod)
f.informers[informerType] = informer
return informer
}
// SharedInformerFactory provides shared informers for resources in all known
// API group versions.
type SharedInformerFactory interface {
internalinterfaces.SharedInformerFactory
ForResource(resource schema.GroupVersionResource) (GenericInformer, error)
WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
Kilo() kilo.Interface
}
func (f *sharedInformerFactory) Kilo() kilo.Interface {
return kilo.New(f, f.namespace, f.tweakListOptions)
}

View File

@ -0,0 +1,60 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by informer-gen. DO NOT EDIT.
package informers
import (
"fmt"
v1alpha1 "github.com/squat/kilo/pkg/k8s/apis/kilo/v1alpha1"
schema "k8s.io/apimachinery/pkg/runtime/schema"
cache "k8s.io/client-go/tools/cache"
)
// GenericInformer is type of SharedIndexInformer which will locate and delegate to other
// sharedInformers based on type
type GenericInformer interface {
Informer() cache.SharedIndexInformer
Lister() cache.GenericLister
}
type genericInformer struct {
informer cache.SharedIndexInformer
resource schema.GroupResource
}
// Informer returns the SharedIndexInformer.
func (f *genericInformer) Informer() cache.SharedIndexInformer {
return f.informer
}
// Lister returns the GenericLister.
func (f *genericInformer) Lister() cache.GenericLister {
return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource)
}
// ForResource gives generic access to a shared informer of the matching type
// TODO extend this to unknown resources with a client pool
func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) {
switch resource {
// Group=kilo, Version=v1alpha1
case v1alpha1.SchemeGroupVersion.WithResource("peers"):
return &genericInformer{resource: resource.GroupResource(), informer: f.Kilo().V1alpha1().Peers().Informer()}, nil
}
return nil, fmt.Errorf("no informer found for %v", resource)
}

View File

@ -0,0 +1,38 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by informer-gen. DO NOT EDIT.
package internalinterfaces
import (
time "time"
versioned "github.com/squat/kilo/pkg/k8s/clientset/versioned"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
cache "k8s.io/client-go/tools/cache"
)
// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer.
type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer
// SharedInformerFactory a small interface to allow for adding an informer without an import cycle
type SharedInformerFactory interface {
Start(stopCh <-chan struct{})
InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer
}
// TweakListOptionsFunc is a function that transforms a v1.ListOptions.
type TweakListOptionsFunc func(*v1.ListOptions)

View File

@ -0,0 +1,44 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by informer-gen. DO NOT EDIT.
package kilo
import (
internalinterfaces "github.com/squat/kilo/pkg/k8s/informers/internalinterfaces"
v1alpha1 "github.com/squat/kilo/pkg/k8s/informers/kilo/v1alpha1"
)
// Interface provides access to each of this group's versions.
type Interface interface {
// V1alpha1 provides access to shared informers for resources in V1alpha1.
V1alpha1() v1alpha1.Interface
}
type group struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// V1alpha1 returns a new v1alpha1.Interface.
func (g *group) V1alpha1() v1alpha1.Interface {
return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions)
}

View File

@ -0,0 +1,43 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by informer-gen. DO NOT EDIT.
package v1alpha1
import (
internalinterfaces "github.com/squat/kilo/pkg/k8s/informers/internalinterfaces"
)
// Interface provides access to all the informers in this group version.
type Interface interface {
// Peers returns a PeerInformer.
Peers() PeerInformer
}
type version struct {
factory internalinterfaces.SharedInformerFactory
namespace string
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// New returns a new Interface.
func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface {
return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions}
}
// Peers returns a PeerInformer.
func (v *version) Peers() PeerInformer {
return &peerInformer{factory: v.factory, tweakListOptions: v.tweakListOptions}
}

View File

@ -0,0 +1,86 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by informer-gen. DO NOT EDIT.
package v1alpha1
import (
time "time"
kilov1alpha1 "github.com/squat/kilo/pkg/k8s/apis/kilo/v1alpha1"
versioned "github.com/squat/kilo/pkg/k8s/clientset/versioned"
internalinterfaces "github.com/squat/kilo/pkg/k8s/informers/internalinterfaces"
v1alpha1 "github.com/squat/kilo/pkg/k8s/listers/kilo/v1alpha1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
watch "k8s.io/apimachinery/pkg/watch"
cache "k8s.io/client-go/tools/cache"
)
// PeerInformer provides access to a shared informer and lister for
// Peers.
type PeerInformer interface {
Informer() cache.SharedIndexInformer
Lister() v1alpha1.PeerLister
}
type peerInformer struct {
factory internalinterfaces.SharedInformerFactory
tweakListOptions internalinterfaces.TweakListOptionsFunc
}
// NewPeerInformer constructs a new informer for Peer type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewPeerInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer {
return NewFilteredPeerInformer(client, resyncPeriod, indexers, nil)
}
// NewFilteredPeerInformer constructs a new informer for Peer type.
// Always prefer using an informer factory to get a shared informer instead of getting an independent
// one. This reduces memory footprint and number of connections to the server.
func NewFilteredPeerInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer {
return cache.NewSharedIndexInformer(
&cache.ListWatch{
ListFunc: func(options v1.ListOptions) (runtime.Object, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.KiloV1alpha1().Peers().List(options)
},
WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {
if tweakListOptions != nil {
tweakListOptions(&options)
}
return client.KiloV1alpha1().Peers().Watch(options)
},
},
&kilov1alpha1.Peer{},
resyncPeriod,
indexers,
)
}
func (f *peerInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer {
return NewFilteredPeerInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions)
}
func (f *peerInformer) Informer() cache.SharedIndexInformer {
return f.factory.InformerFor(&kilov1alpha1.Peer{}, f.defaultInformer)
}
func (f *peerInformer) Lister() v1alpha1.PeerLister {
return v1alpha1.NewPeerLister(f.Informer().GetIndexer())
}

View File

@ -0,0 +1,21 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by lister-gen. DO NOT EDIT.
package v1alpha1
// PeerListerExpansion allows custom methods to be added to
// PeerLister.
type PeerListerExpansion interface{}

View File

@ -0,0 +1,63 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Code generated by lister-gen. DO NOT EDIT.
package v1alpha1
import (
v1alpha1 "github.com/squat/kilo/pkg/k8s/apis/kilo/v1alpha1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/cache"
)
// PeerLister helps list Peers.
type PeerLister interface {
// List lists all Peers in the indexer.
List(selector labels.Selector) (ret []*v1alpha1.Peer, err error)
// Get retrieves the Peer from the index for a given name.
Get(name string) (*v1alpha1.Peer, error)
PeerListerExpansion
}
// peerLister implements the PeerLister interface.
type peerLister struct {
indexer cache.Indexer
}
// NewPeerLister returns a new PeerLister.
func NewPeerLister(indexer cache.Indexer) PeerLister {
return &peerLister{indexer: indexer}
}
// List lists all Peers in the indexer.
func (s *peerLister) List(selector labels.Selector) (ret []*v1alpha1.Peer, err error) {
err = cache.ListAll(s.indexer, selector, func(m interface{}) {
ret = append(ret, m.(*v1alpha1.Peer))
})
return ret, err
}
// Get retrieves the Peer from the index for a given name.
func (s *peerLister) Get(name string) (*v1alpha1.Peer, error) {
obj, exists, err := s.indexer.GetByKey(name)
if err != nil {
return nil, err
}
if !exists {
return nil, errors.NewNotFound(v1alpha1.Resource("peer"), name)
}
return obj.(*v1alpha1.Peer), nil
}

View File

@ -37,22 +37,22 @@ func (t *Topology) Dot() (string, error) {
if err := g.SetDir(true); err != nil {
return "", fmt.Errorf("failed to set direction")
}
leaders := make([]string, len(t.Segments))
leaders := make([]string, len(t.segments))
nodeAttrs := map[string]string{
string(gographviz.Shape): "ellipse",
}
for i, s := range t.Segments {
if err := g.AddSubGraph("kilo", subGraphName(s.Location), nil); err != nil {
for i, s := range t.segments {
if err := g.AddSubGraph("kilo", subGraphName(s.location), nil); err != nil {
return "", fmt.Errorf("failed to add subgraph")
}
if err := g.AddAttr(subGraphName(s.Location), string(gographviz.Label), graphEscape(s.Location)); err != nil {
if err := g.AddAttr(subGraphName(s.location), string(gographviz.Label), graphEscape(s.location)); err != nil {
return "", fmt.Errorf("failed to add label to subgraph")
}
if err := g.AddAttr(subGraphName(s.Location), string(gographviz.Style), `"dashed,rounded"`); err != nil {
if err := g.AddAttr(subGraphName(s.location), string(gographviz.Style), `"dashed,rounded"`); err != nil {
return "", fmt.Errorf("failed to add style to subgraph")
}
for j := range s.cidrs {
if err := g.AddNode(subGraphName(s.Location), graphEscape(s.hostnames[j]), nodeAttrs); err != nil {
if err := g.AddNode(subGraphName(s.location), graphEscape(s.hostnames[j]), nodeAttrs); err != nil {
return "", fmt.Errorf("failed to add node to subgraph")
}
var wg net.IP
@ -62,11 +62,11 @@ func (t *Topology) Dot() (string, error) {
return "", fmt.Errorf("failed to add rank to node")
}
}
if err := g.Nodes.Lookup[graphEscape(s.hostnames[j])].Attrs.Add(string(gographviz.Label), nodeLabel(s.Location, s.hostnames[j], s.cidrs[j], s.privateIPs[j], wg)); err != nil {
if err := g.Nodes.Lookup[graphEscape(s.hostnames[j])].Attrs.Add(string(gographviz.Label), nodeLabel(s.location, s.hostnames[j], s.cidrs[j], s.privateIPs[j], wg)); err != nil {
return "", fmt.Errorf("failed to add label to node")
}
}
meshSubGraph(g, g.Relations.SortedChildren(subGraphName(s.Location)), s.leader)
meshSubGraph(g, g.Relations.SortedChildren(subGraphName(s.location)), s.leader)
leaders[i] = graphEscape(s.hostnames[s.leader])
}
meshSubGraph(g, leaders, 0)

View File

@ -15,6 +15,7 @@
package mesh
import (
"bytes"
"fmt"
"io/ioutil"
"net"
@ -43,6 +44,8 @@ const (
PrivateKeyPath = KiloPath + "/key"
// ConfPath is the filepath where the WireGuard configuration is stored.
ConfPath = KiloPath + "/conf"
// DefaultKiloPort is the default UDP port Kilo uses.
DefaultKiloPort = 51820
)
// Granularity represents the abstraction level at which the network
@ -93,6 +96,17 @@ func (n *Node) Ready() bool {
return n != nil && n.ExternalIP != nil && n.Key != nil && n.InternalIP != nil && n.Subnet != nil && time.Now().Unix()-n.LastSeen < int64(resyncPeriod)*2/int64(time.Second)
}
// Peer represents a peer in the network.
type Peer struct {
wireguard.Peer
Name string
}
// Ready indicates whether or not the peer is ready.
func (p *Peer) Ready() bool {
return p != nil && p.AllowedIPs != nil && len(p.AllowedIPs) != 0 && p.PublicKey != nil
}
// EventType describes what kind of an action an event represents.
type EventType string
@ -105,24 +119,53 @@ const (
UpdateEvent EventType = "update"
)
// Event represents an update event concerning a node in the cluster.
type Event struct {
// NodeEvent represents an event concerning a node in the cluster.
type NodeEvent struct {
Type EventType
Node *Node
}
// Backend can get nodes by name, init itself,
// PeerEvent represents an event concerning a peer in the cluster.
type PeerEvent struct {
Type EventType
Peer *Peer
}
// Backend can create clients for all of the
// primitive types that Kilo deals with, namely:
// * nodes; and
// * peers.
type Backend interface {
Nodes() NodeBackend
Peers() PeerBackend
}
// NodeBackend can get nodes by name, init itself,
// list the nodes that should be meshed,
// set Kilo properties for a node,
// clean up any changes applied to the backend,
// and watch for changes to nodes.
type Backend interface {
type NodeBackend interface {
CleanUp(string) error
Get(string) (*Node, error)
Init(<-chan struct{}) error
List() ([]*Node, error)
Set(string, *Node) error
Watch() <-chan *Event
Watch() <-chan *NodeEvent
}
// PeerBackend can get peers by name, init itself,
// list the peers that should be in the mesh,
// set fields for a peer,
// clean up any changes applied to the backend,
// and watch for changes to peers.
type PeerBackend interface {
CleanUp(string) error
Get(string) (*Peer, error)
Init(<-chan struct{}) error
List() ([]*Peer, error)
Set(string, *Peer) error
Watch() <-chan *PeerEvent
}
// Mesh is able to create Kilo network meshes.
@ -138,7 +181,7 @@ type Mesh struct {
kiloIface int
key []byte
local bool
port int
port uint32
priv []byte
privIface int
pub []byte
@ -148,23 +191,26 @@ type Mesh struct {
table *route.Table
tunlIface int
// nodes is a mutable field in the struct
// nodes and peers are mutable fields in the struct
// and needs to be guarded.
nodes map[string]*Node
peers map[string]*Peer
mu sync.Mutex
errorCounter *prometheus.CounterVec
nodesGuage prometheus.Gauge
peersGuage prometheus.Gauge
reconcileCounter prometheus.Counter
logger log.Logger
}
// New returns a new Mesh instance.
func New(backend Backend, encapsulate Encapsulate, granularity Granularity, hostname string, port int, subnet *net.IPNet, local bool, logger log.Logger) (*Mesh, error) {
func New(backend Backend, encapsulate Encapsulate, granularity Granularity, hostname string, port uint32, subnet *net.IPNet, local bool, logger log.Logger) (*Mesh, error) {
if err := os.MkdirAll(KiloPath, 0700); err != nil {
return nil, fmt.Errorf("failed to create directory to store configuration: %v", err)
}
private, err := ioutil.ReadFile(PrivateKeyPath)
private = bytes.Trim(private, "\n")
if err != nil {
level.Warn(logger).Log("msg", "no private key found on disk; generating one now")
if private, err = wireguard.GenKey(); err != nil {
@ -224,6 +270,7 @@ func New(backend Backend, encapsulate Encapsulate, granularity Granularity, host
ipTables: ipTables,
kiloIface: kiloIface,
nodes: make(map[string]*Node),
peers: make(map[string]*Peer),
port: port,
priv: private,
privIface: privIface,
@ -240,7 +287,11 @@ func New(backend Backend, encapsulate Encapsulate, granularity Granularity, host
}, []string{"event"}),
nodesGuage: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "kilo_nodes",
Help: "Number of in the mesh.",
Help: "Number of nodes in the mesh.",
}),
peersGuage: prometheus.NewGauge(prometheus.GaugeOpts{
Name: "kilo_peers",
Help: "Number of peers in the mesh.",
}),
reconcileCounter: prometheus.NewCounter(prometheus.CounterOpts{
Name: "kilo_reconciles_total",
@ -252,8 +303,11 @@ func New(backend Backend, encapsulate Encapsulate, granularity Granularity, host
// Run starts the mesh.
func (m *Mesh) Run() error {
if err := m.Init(m.stop); err != nil {
return fmt.Errorf("failed to initialize backend: %v", err)
if err := m.Nodes().Init(m.stop); err != nil {
return fmt.Errorf("failed to initialize node backend: %v", err)
}
if err := m.Peers().Init(m.stop); err != nil {
return fmt.Errorf("failed to initialize peer backend: %v", err)
}
ipsetErrors, err := m.ipset.Run(m.stop)
if err != nil {
@ -285,14 +339,19 @@ func (m *Mesh) Run() error {
}()
defer m.cleanUp()
t := time.NewTimer(resyncPeriod)
w := m.Watch()
nw := m.Nodes().Watch()
pw := m.Peers().Watch()
var ne *NodeEvent
var pe *PeerEvent
for {
var e *Event
select {
case e = <-w:
m.sync(e)
case ne = <-nw:
m.syncNodes(ne)
case pe = <-pw:
m.syncPeers(pe)
case <-t.C:
m.checkIn()
m.syncEndpoints()
m.applyTopology()
t.Reset(resyncPeriod)
case <-m.stop:
@ -301,9 +360,50 @@ func (m *Mesh) Run() error {
}
}
func (m *Mesh) sync(e *Event) {
// WireGuard updates the endpoints of peers to match the
// last place a valid packet was received from.
// Periodically we need to syncronize the endpoints
// of peers in the backend to match the WireGuard configuration.
func (m *Mesh) syncEndpoints() {
link, err := linkByIndex(m.kiloIface)
if err != nil {
level.Error(m.logger).Log("error", err)
m.errorCounter.WithLabelValues("endpoints").Inc()
return
}
conf, err := wireguard.ShowConf(link.Attrs().Name)
if err != nil {
level.Error(m.logger).Log("error", err)
m.errorCounter.WithLabelValues("endpoints").Inc()
return
}
m.mu.Lock()
defer m.mu.Unlock()
c := wireguard.Parse(conf)
var key string
var tmp *Peer
for i := range c.Peers {
// Peers are indexed by public key.
key = string(c.Peers[i].PublicKey)
if p, ok := m.peers[key]; ok {
tmp = &Peer{
Name: p.Name,
Peer: *c.Peers[i],
}
if !peersAreEqual(tmp, p) {
p.Endpoint = tmp.Endpoint
if err := m.Peers().Set(p.Name, p); err != nil {
level.Error(m.logger).Log("error", err)
m.errorCounter.WithLabelValues("endpoints").Inc()
}
}
}
}
}
func (m *Mesh) syncNodes(e *NodeEvent) {
logger := log.With(m.logger, "event", e.Type)
level.Debug(logger).Log("msg", "syncing", "event", e.Type)
level.Debug(logger).Log("msg", "syncing nodes", "event", e.Type)
if isSelf(m.hostname, e.Node) {
level.Debug(logger).Log("msg", "processing local node", "node", e.Node)
m.handleLocal(e.Node)
@ -326,9 +426,11 @@ func (m *Mesh) sync(e *Event) {
fallthrough
case UpdateEvent:
if !nodesAreEqual(m.nodes[e.Node.Name], e.Node) {
m.nodes[e.Node.Name] = e.Node
diff = true
}
// Even if the nodes are the same,
// overwrite the old node to update the timestamp.
m.nodes[e.Node.Name] = e.Node
case DeleteEvent:
delete(m.nodes, e.Node.Name)
diff = true
@ -341,6 +443,43 @@ func (m *Mesh) sync(e *Event) {
}
}
func (m *Mesh) syncPeers(e *PeerEvent) {
logger := log.With(m.logger, "event", e.Type)
level.Debug(logger).Log("msg", "syncing peers", "event", e.Type)
var diff bool
m.mu.Lock()
// Peers are indexed by public key.
key := string(e.Peer.PublicKey)
if !e.Peer.Ready() {
level.Debug(logger).Log("msg", "received incomplete peer", "peer", e.Peer)
// An existing peer is no longer valid
// so remove it from the mesh.
if _, ok := m.peers[key]; ok {
level.Info(logger).Log("msg", "peer is no longer in the mesh", "peer", e.Peer)
delete(m.peers, key)
diff = true
}
} else {
switch e.Type {
case AddEvent:
fallthrough
case UpdateEvent:
if !peersAreEqual(m.peers[key], e.Peer) {
m.peers[key] = e.Peer
diff = true
}
case DeleteEvent:
delete(m.peers, key)
diff = true
}
}
m.mu.Unlock()
if diff {
level.Info(logger).Log("peer", e.Peer)
m.applyTopology()
}
}
// checkIn will try to update the local node's LastSeen timestamp
// in the backend.
func (m *Mesh) checkIn() {
@ -352,7 +491,7 @@ func (m *Mesh) checkIn() {
return
}
n.LastSeen = time.Now().Unix()
if err := m.Set(m.hostname, n); err != nil {
if err := m.Nodes().Set(m.hostname, n); err != nil {
level.Error(m.logger).Log("error", fmt.Sprintf("failed to set local node: %v", err), "node", n)
m.errorCounter.WithLabelValues("checkin").Inc()
return
@ -380,7 +519,7 @@ func (m *Mesh) handleLocal(n *Node) {
}
if !nodesAreEqual(n, local) {
level.Debug(m.logger).Log("msg", "local node differs from backend")
if err := m.Set(m.hostname, local); err != nil {
if err := m.Nodes().Set(m.hostname, local); err != nil {
level.Error(m.logger).Log("error", fmt.Sprintf("failed to set local node: %v", err), "node", local)
m.errorCounter.WithLabelValues("local").Inc()
return
@ -406,31 +545,42 @@ func (m *Mesh) applyTopology() {
m.mu.Lock()
defer m.mu.Unlock()
// Ensure all unready nodes are removed.
var ready float64
for n := range m.nodes {
if !m.nodes[n].Ready() {
delete(m.nodes, n)
var readyNodes float64
for k := range m.nodes {
if !m.nodes[k].Ready() {
delete(m.nodes, k)
continue
}
ready++
readyNodes++
}
m.nodesGuage.Set(ready)
// Ensure all unready peers are removed.
var readyPeers float64
for k := range m.peers {
if !m.peers[k].Ready() {
delete(m.peers, k)
continue
}
readyPeers++
}
m.nodesGuage.Set(readyNodes)
m.peersGuage.Set(readyPeers)
// We cannot do anything with the topology until the local node is available.
if m.nodes[m.hostname] == nil {
return
}
t, err := NewTopology(m.nodes, m.granularity, m.hostname, m.port, m.priv, m.subnet)
t, err := NewTopology(m.nodes, m.peers, m.granularity, m.hostname, m.port, m.priv, m.subnet)
if err != nil {
level.Error(m.logger).Log("error", err)
m.errorCounter.WithLabelValues("apply").Inc()
return
}
conf, err := t.Conf()
conf := t.Conf()
buf, err := conf.Bytes()
if err != nil {
level.Error(m.logger).Log("error", err)
m.errorCounter.WithLabelValues("apply").Inc()
}
if err := ioutil.WriteFile(ConfPath, conf, 0600); err != nil {
if err := ioutil.WriteFile(ConfPath, buf, 0600); err != nil {
level.Error(m.logger).Log("error", err)
m.errorCounter.WithLabelValues("apply").Inc()
return
@ -443,6 +593,9 @@ func (m *Mesh) applyTopology() {
}
rules := iptables.MasqueradeRules(private, m.nodes[m.hostname].Subnet, t.RemoteSubnets())
rules = append(rules, iptables.ForwardRules(m.subnet)...)
for _, p := range m.peers {
rules = append(rules, iptables.ForwardRules(p.AllowedIPs...)...)
}
if err := m.ipTables.Set(rules); err != nil {
level.Error(m.logger).Log("error", err)
m.errorCounter.WithLabelValues("apply").Inc()
@ -450,8 +603,8 @@ func (m *Mesh) applyTopology() {
}
if m.encapsulate != NeverEncapsulate {
var peers []net.IP
for _, s := range t.Segments {
if s.Location == m.nodes[m.hostname].Location {
for _, s := range t.segments {
if s.location == m.nodes[m.hostname].Location {
peers = s.privateIPs
break
}
@ -461,6 +614,8 @@ func (m *Mesh) applyTopology() {
m.errorCounter.WithLabelValues("apply").Inc()
return
}
// If we are handling local routes, ensure the local
// tunnel has an IP address.
if m.local {
if err := iproute.SetAddress(m.tunlIface, oneAddressCIDR(newAllocator(*m.nodes[m.hostname].Subnet).next().IP)); err != nil {
level.Error(m.logger).Log("error", err)
@ -489,14 +644,9 @@ func (m *Mesh) applyTopology() {
}
// Setting the WireGuard configuration interrupts existing connections
// so only set the configuration if it has changed.
equal, err := wireguard.CompareConf(conf, oldConf)
if err != nil {
level.Error(m.logger).Log("error", err)
m.errorCounter.WithLabelValues("apply").Inc()
// Don't return here, simply overwrite the old configuration.
equal = false
}
equal := conf.Equal(wireguard.Parse(oldConf))
if !equal {
level.Info(m.logger).Log("msg", "WireGuard configurations are different")
if err := wireguard.SetConf(link.Attrs().Name, ConfPath); err != nil {
level.Error(m.logger).Log("error", err)
m.errorCounter.WithLabelValues("apply").Inc()
@ -531,6 +681,7 @@ func (m *Mesh) RegisterMetrics(r prometheus.Registerer) {
r.MustRegister(
m.errorCounter,
m.nodesGuage,
m.peersGuage,
m.reconcileCounter,
)
}
@ -558,11 +709,15 @@ func (m *Mesh) cleanUp() {
m.errorCounter.WithLabelValues("cleanUp").Inc()
}
if err := iproute.RemoveInterface(m.kiloIface); err != nil {
level.Error(m.logger).Log("error", fmt.Sprintf("failed to remove wireguard interface: %v", err))
level.Error(m.logger).Log("error", fmt.Sprintf("failed to remove WireGuard interface: %v", err))
m.errorCounter.WithLabelValues("cleanUp").Inc()
}
if err := m.CleanUp(m.hostname); err != nil {
level.Error(m.logger).Log("error", fmt.Sprintf("failed to clean up backend: %v", err))
if err := m.Nodes().CleanUp(m.hostname); err != nil {
level.Error(m.logger).Log("error", fmt.Sprintf("failed to clean up node backend: %v", err))
m.errorCounter.WithLabelValues("cleanUp").Inc()
}
if err := m.Peers().CleanUp(m.hostname); err != nil {
level.Error(m.logger).Log("error", fmt.Sprintf("failed to clean up peer backend: %v", err))
m.errorCounter.WithLabelValues("cleanUp").Inc()
}
if err := m.ipset.CleanUp(); err != nil {
@ -586,6 +741,32 @@ func nodesAreEqual(a, b *Node) bool {
return ipNetsEqual(a.ExternalIP, b.ExternalIP) && string(a.Key) == string(b.Key) && ipNetsEqual(a.InternalIP, b.InternalIP) && a.Leader == b.Leader && a.Location == b.Location && a.Name == b.Name && subnetsEqual(a.Subnet, b.Subnet)
}
func peersAreEqual(a, b *Peer) bool {
if !(a != nil) == (b != nil) {
return false
}
if a == b {
return true
}
if !(a.Endpoint != nil) == (b.Endpoint != nil) {
return false
}
if a.Endpoint != nil {
if !a.Endpoint.IP.Equal(b.Endpoint.IP) || a.Endpoint.Port != b.Endpoint.Port {
return false
}
}
if len(a.AllowedIPs) != len(b.AllowedIPs) {
return false
}
for i := range a.AllowedIPs {
if !ipNetsEqual(a.AllowedIPs[i], b.AllowedIPs[i]) {
return false
}
}
return string(a.PublicKey) == string(b.PublicKey) && a.PersistentKeepalive == b.PersistentKeepalive
}
func ipNetsEqual(a, b *net.IPNet) bool {
if a == nil && b == nil {
return true

View File

@ -15,41 +15,24 @@
package mesh
import (
"bytes"
"errors"
"fmt"
"net"
"sort"
"strings"
"text/template"
"github.com/squat/kilo/pkg/wireguard"
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
)
var (
confTemplate = template.Must(template.New("").Parse(`[Interface]
PrivateKey = {{.Key}}
ListenPort = {{.Port}}
{{range .Segments -}}
{{if ne .Location $.Location}}
[Peer]
PublicKey = {{.Key}}
Endpoint = {{.Endpoint}}:{{$.Port}}
AllowedIPs = {{.AllowedIPs}}
{{end}}
{{- end -}}
`))
)
// Topology represents the logical structure of the overlay network.
type Topology struct {
// Some fields need to be exported so that the template can read them.
Key string
Port int
// key is the private key of the node creating the topology.
key []byte
port uint32
// Location is the logical location of the local host.
Location string
Segments []*segment
location string
segments []*segment
peers []*Peer
// hostname is the hostname of the local host.
hostname string
@ -69,11 +52,11 @@ type Topology struct {
type segment struct {
// Some fields need to be exported so that the template can read them.
AllowedIPs string
Endpoint string
Key string
allowedIPs []*net.IPNet
endpoint net.IP
key []byte
// Location is the logical location of this segment.
Location string
location string
// cidrs is a slice of subnets of all peers in the segment.
cidrs []*net.IPNet
@ -88,8 +71,8 @@ type segment struct {
wireGuardIP net.IP
}
// NewTopology creates a new Topology struct from a given set of nodes.
func NewTopology(nodes map[string]*Node, granularity Granularity, hostname string, port int, key []byte, subnet *net.IPNet) (*Topology, error) {
// NewTopology creates a new Topology struct from a given set of nodes and peers.
func NewTopology(nodes map[string]*Node, peers map[string]*Peer, granularity Granularity, hostname string, port uint32, key []byte, subnet *net.IPNet) (*Topology, error) {
topoMap := make(map[string][]*Node)
for _, node := range nodes {
var location string
@ -109,7 +92,7 @@ func NewTopology(nodes map[string]*Node, granularity Granularity, hostname strin
localLocation = hostname
}
t := Topology{Key: strings.TrimSpace(string(key)), Port: port, hostname: hostname, Location: localLocation, subnet: subnet, privateIP: nodes[hostname].InternalIP}
t := Topology{key: key, port: port, hostname: hostname, location: localLocation, subnet: subnet, privateIP: nodes[hostname].InternalIP}
for location := range topoMap {
// Sort the location so the result is stable.
sort.Slice(topoMap[location], func(i, j int) bool {
@ -119,7 +102,7 @@ func NewTopology(nodes map[string]*Node, granularity Granularity, hostname strin
if location == localLocation && topoMap[location][leader].Name == hostname {
t.leader = true
}
var allowedIPs []string
var allowedIPs []*net.IPNet
var cidrs []*net.IPNet
var hostnames []string
var privateIPs []net.IP
@ -128,37 +111,45 @@ func NewTopology(nodes map[string]*Node, granularity Granularity, hostname strin
// - the node's allocated subnet
// - the node's WireGuard IP
// - the node's internal IP
allowedIPs = append(allowedIPs, node.Subnet.String(), oneAddressCIDR(node.InternalIP.IP).String())
allowedIPs = append(allowedIPs, node.Subnet, oneAddressCIDR(node.InternalIP.IP))
cidrs = append(cidrs, node.Subnet)
hostnames = append(hostnames, node.Name)
privateIPs = append(privateIPs, node.InternalIP.IP)
}
t.Segments = append(t.Segments, &segment{
AllowedIPs: strings.Join(allowedIPs, ", "),
Endpoint: topoMap[location][leader].ExternalIP.IP.String(),
Key: strings.TrimSpace(string(topoMap[location][leader].Key)),
Location: location,
t.segments = append(t.segments, &segment{
allowedIPs: allowedIPs,
endpoint: topoMap[location][leader].ExternalIP.IP,
key: topoMap[location][leader].Key,
location: location,
cidrs: cidrs,
hostnames: hostnames,
leader: leader,
privateIPs: privateIPs,
})
}
// Sort the Topology so the result is stable.
sort.Slice(t.Segments, func(i, j int) bool {
return t.Segments[i].Location < t.Segments[j].Location
// Sort the Topology segments so the result is stable.
sort.Slice(t.segments, func(i, j int) bool {
return t.segments[i].location < t.segments[j].location
})
for _, peer := range peers {
t.peers = append(t.peers, peer)
}
// Sort the Topology peers so the result is stable.
sort.Slice(t.peers, func(i, j int) bool {
return t.peers[i].Name < t.peers[j].Name
})
// Allocate IPs to the segment leaders in a stable, coordination-free manner.
a := newAllocator(*subnet)
for _, segment := range t.Segments {
for _, segment := range t.segments {
ipNet := a.next()
if ipNet == nil {
return nil, errors.New("failed to allocate an IP address; ran out of IP addresses")
}
segment.wireGuardIP = ipNet.IP
segment.AllowedIPs = fmt.Sprintf("%s, %s", segment.AllowedIPs, ipNet.String())
if t.leader && segment.Location == t.Location {
segment.allowedIPs = append(segment.allowedIPs, ipNet)
if t.leader && segment.location == t.location {
t.wireGuardCIDR = &net.IPNet{IP: ipNet.IP, Mask: t.subnet.Mask}
}
}
@ -169,8 +160,8 @@ func NewTopology(nodes map[string]*Node, granularity Granularity, hostname strin
// RemoteSubnets identifies the subnets of the hosts in segments different than the host's.
func (t *Topology) RemoteSubnets() []*net.IPNet {
var remote []*net.IPNet
for _, s := range t.Segments {
if s == nil || s.Location == t.Location {
for _, s := range t.segments {
if s == nil || s.location == t.location {
continue
}
remote = append(remote, s.cidrs...)
@ -184,13 +175,13 @@ func (t *Topology) Routes(kiloIface, privIface, tunlIface int, local bool, encap
if !t.leader {
// Find the leader for this segment.
var leader net.IP
for _, segment := range t.Segments {
if segment.Location == t.Location {
for _, segment := range t.segments {
if segment.location == t.location {
leader = segment.privateIPs[segment.leader]
break
}
}
for _, segment := range t.Segments {
for _, segment := range t.segments {
// First, add a route to the WireGuard IP of the segment.
routes = append(routes, encapsulateRoute(&netlink.Route{
Dst: oneAddressCIDR(segment.wireGuardIP),
@ -200,7 +191,7 @@ func (t *Topology) Routes(kiloIface, privIface, tunlIface int, local bool, encap
Protocol: unix.RTPROT_STATIC,
}, encapsulate, t.privateIP, tunlIface))
// Add routes for the current segment if local is true.
if segment.Location == t.Location {
if segment.location == t.location {
if local {
for i := range segment.cidrs {
// Don't add routes for the local node.
@ -239,11 +230,23 @@ func (t *Topology) Routes(kiloIface, privIface, tunlIface int, local bool, encap
}, encapsulate, t.privateIP, tunlIface))
}
}
// Add routes for the allowed IPs of peers.
for _, peer := range t.peers {
for i := range peer.AllowedIPs {
routes = append(routes, encapsulateRoute(&netlink.Route{
Dst: peer.AllowedIPs[i],
Flags: int(netlink.FLAG_ONLINK),
Gw: leader,
LinkIndex: privIface,
Protocol: unix.RTPROT_STATIC,
}, encapsulate, t.privateIP, tunlIface))
}
}
return routes
}
for _, segment := range t.Segments {
for _, segment := range t.segments {
// Add routes for the current segment if local is true.
if segment.Location == t.Location {
if segment.location == t.location {
if local {
for i := range segment.cidrs {
// Don't add routes for the local node.
@ -282,6 +285,16 @@ func (t *Topology) Routes(kiloIface, privIface, tunlIface int, local bool, encap
})
}
}
// Add routes for the allowed IPs of peers.
for _, peer := range t.peers {
for i := range peer.AllowedIPs {
routes = append(routes, &netlink.Route{
Dst: peer.AllowedIPs[i],
LinkIndex: kiloIface,
Protocol: unix.RTPROT_STATIC,
})
}
}
return routes
}
@ -293,12 +306,66 @@ func encapsulateRoute(route *netlink.Route, encapsulate Encapsulate, subnet *net
}
// Conf generates a WireGuard configuration file for a given Topology.
func (t *Topology) Conf() ([]byte, error) {
conf := new(bytes.Buffer)
if err := confTemplate.Execute(conf, t); err != nil {
return nil, err
func (t *Topology) Conf() *wireguard.Conf {
c := &wireguard.Conf{
Interface: &wireguard.Interface{
PrivateKey: t.key,
ListenPort: t.port,
},
}
return conf.Bytes(), nil
for _, s := range t.segments {
if s.location == t.location {
continue
}
peer := &wireguard.Peer{
AllowedIPs: s.allowedIPs,
Endpoint: &wireguard.Endpoint{
IP: s.endpoint,
Port: uint32(t.port),
},
PublicKey: s.key,
}
c.Peers = append(c.Peers, peer)
}
for _, p := range t.peers {
peer := &wireguard.Peer{
AllowedIPs: p.AllowedIPs,
PersistentKeepalive: p.PersistentKeepalive,
PublicKey: p.PublicKey,
Endpoint: p.Endpoint,
}
c.Peers = append(c.Peers, peer)
}
return c
}
// PeerConf generates a WireGuard configuration file for a given peer in a Topology.
func (t *Topology) PeerConf(name string) *wireguard.Conf {
c := &wireguard.Conf{}
for _, s := range t.segments {
peer := &wireguard.Peer{
AllowedIPs: s.allowedIPs,
Endpoint: &wireguard.Endpoint{
IP: s.endpoint,
Port: uint32(t.port),
},
PublicKey: s.key,
}
c.Peers = append(c.Peers, peer)
}
for _, p := range t.peers {
if p.Name == name {
continue
}
peer := &wireguard.Peer{
AllowedIPs: p.AllowedIPs,
PersistentKeepalive: p.PersistentKeepalive,
PublicKey: p.PublicKey,
Endpoint: p.Endpoint,
}
c.Peers = append(c.Peers, peer)
}
return c
}
// oneAddressCIDR takes an IP address and returns a CIDR

File diff suppressed because it is too large Load Diff

391
pkg/wireguard/conf.go Normal file
View File

@ -0,0 +1,391 @@
// Copyright 2019 the Kilo authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package wireguard
import (
"bufio"
"bytes"
"fmt"
"net"
"sort"
"strconv"
"strings"
)
type section string
type key string
const (
separator = "="
interfaceSection section = "Interface"
peerSection section = "Peer"
listenPortKey key = "ListenPort"
allowedIPsKey key = "AllowedIPs"
endpointKey key = "Endpoint"
persistentKeepaliveKey key = "PersistentKeepalive"
privateKeyKey key = "PrivateKey"
publicKeyKey key = "PublicKey"
)
// Conf represents a WireGuard configuration file.
type Conf struct {
Interface *Interface
Peers []*Peer
}
// Interface represents the `interface` section of a WireGuard configuration.
type Interface struct {
ListenPort uint32
PrivateKey []byte
}
// Peer represents a `peer` section of a WireGuard configuration.
type Peer struct {
AllowedIPs []*net.IPNet
Endpoint *Endpoint
PersistentKeepalive int
PublicKey []byte
}
// Endpoint represents an `endpoint` key of a `peer` section.
type Endpoint struct {
IP net.IP
Port uint32
}
// Parse parses a given WireGuard configuration file and produces a Conf struct.
func Parse(buf []byte) *Conf {
var (
active section
ai *net.IPNet
kv []string
c Conf
err error
iface *Interface
i int
ip, ip4 net.IP
k key
line, v string
peer *Peer
port uint64
)
s := bufio.NewScanner(bytes.NewBuffer(buf))
for s.Scan() {
line = strings.TrimSpace(s.Text())
// Skip comments.
if strings.HasPrefix(line, "#") {
continue
}
// Line is a section title.
if strings.HasPrefix(line, "[") {
if peer != nil {
c.Peers = append(c.Peers, peer)
peer = nil
}
if iface != nil {
c.Interface = iface
iface = nil
}
active = section(strings.TrimSpace(strings.Trim(line, "[]")))
switch active {
case interfaceSection:
iface = new(Interface)
case peerSection:
peer = new(Peer)
}
continue
}
kv = strings.SplitN(line, separator, 2)
if len(kv) != 2 {
continue
}
k = key(strings.TrimSpace(kv[0]))
v = strings.TrimSpace(kv[1])
switch active {
case interfaceSection:
switch k {
case listenPortKey:
port, err = strconv.ParseUint(v, 10, 32)
if err != nil {
continue
}
iface.ListenPort = uint32(port)
case privateKeyKey:
iface.PrivateKey = []byte(v)
}
case peerSection:
switch k {
case allowedIPsKey:
// Reuse string slice.
kv = strings.Split(v, ",")
for i = range kv {
ip, ai, err = net.ParseCIDR(strings.TrimSpace(kv[i]))
if err != nil {
continue
}
if ip4 = ip.To4(); ip4 != nil {
ip = ip4
} else {
ip = ip.To16()
}
ai.IP = ip
peer.AllowedIPs = append(peer.AllowedIPs, ai)
}
case endpointKey:
// Reuse string slice.
kv = strings.Split(v, ":")
if len(kv) != 2 {
continue
}
ip = net.ParseIP(kv[0])
if ip == nil {
continue
}
port, err = strconv.ParseUint(kv[1], 10, 32)
if err != nil {
continue
}
if ip4 = ip.To4(); ip4 != nil {
ip = ip4
} else {
ip = ip.To16()
}
peer.Endpoint = &Endpoint{
IP: ip,
Port: uint32(port),
}
case persistentKeepaliveKey:
i, err = strconv.Atoi(v)
if err != nil {
continue
}
peer.PersistentKeepalive = i
case publicKeyKey:
peer.PublicKey = []byte(v)
}
}
}
if peer != nil {
c.Peers = append(c.Peers, peer)
}
if iface != nil {
c.Interface = iface
}
return &c
}
// Bytes renders a WireGuard configuration to bytes.
func (c *Conf) Bytes() ([]byte, error) {
var err error
buf := bytes.NewBuffer(make([]byte, 0, 512))
if c.Interface != nil {
if err = writeSection(buf, interfaceSection); err != nil {
return nil, fmt.Errorf("failed to write interface: %v", err)
}
if err = writePKey(buf, privateKeyKey, c.Interface.PrivateKey); err != nil {
return nil, fmt.Errorf("failed to write private key: %v", err)
}
if err = writeValue(buf, listenPortKey, strconv.FormatUint(uint64(c.Interface.ListenPort), 10)); err != nil {
return nil, fmt.Errorf("failed to write listen port: %v", err)
}
}
for i, p := range c.Peers {
// Add newlines to make the formatting nicer.
if i == 0 && c.Interface != nil || i != 0 {
if err = buf.WriteByte('\n'); err != nil {
return nil, err
}
}
if err = writeSection(buf, peerSection); err != nil {
return nil, fmt.Errorf("failed to write interface: %v", err)
}
if err = writeAllowedIPs(buf, p.AllowedIPs); err != nil {
return nil, fmt.Errorf("failed to write allowed IPs: %v", err)
}
if err = writeEndpoint(buf, p.Endpoint); err != nil {
return nil, fmt.Errorf("failed to write endpoint: %v", err)
}
if err = writeValue(buf, persistentKeepaliveKey, strconv.Itoa(p.PersistentKeepalive)); err != nil {
return nil, fmt.Errorf("failed to write persistent keepalive: %v", err)
}
if err = writePKey(buf, publicKeyKey, p.PublicKey); err != nil {
return nil, fmt.Errorf("failed to write public key: %v", err)
}
}
return buf.Bytes(), nil
}
// Equal checks if two WireGuare configurations are equivalent.
func (c *Conf) Equal(b *Conf) bool {
if (c.Interface == nil) != (b.Interface == nil) {
return false
}
if c.Interface != nil {
if c.Interface.ListenPort != b.Interface.ListenPort || !bytes.Equal(c.Interface.PrivateKey, b.Interface.PrivateKey) {
return false
}
}
if len(c.Peers) != len(b.Peers) {
return false
}
sortPeers(c.Peers)
sortPeers(b.Peers)
for i := range c.Peers {
if len(c.Peers[i].AllowedIPs) != len(b.Peers[i].AllowedIPs) {
return false
}
sortCIDRs(c.Peers[i].AllowedIPs)
sortCIDRs(b.Peers[i].AllowedIPs)
for j := range c.Peers[i].AllowedIPs {
if c.Peers[i].AllowedIPs[j].String() != b.Peers[i].AllowedIPs[j].String() {
return false
}
}
if (c.Peers[i].Endpoint == nil) != (b.Peers[i].Endpoint == nil) {
return false
}
if c.Peers[i].Endpoint != nil {
if !c.Peers[i].Endpoint.IP.Equal(b.Peers[i].Endpoint.IP) || c.Peers[i].Endpoint.Port != b.Peers[i].Endpoint.Port {
return false
}
}
if c.Peers[i].PersistentKeepalive != b.Peers[i].PersistentKeepalive || !bytes.Equal(c.Peers[i].PublicKey, b.Peers[i].PublicKey) {
return false
}
}
return true
}
func sortPeers(peers []*Peer) {
sort.Slice(peers, func(i, j int) bool {
if bytes.Compare(peers[i].PublicKey, peers[j].PublicKey) < 0 {
return true
}
return false
})
}
func sortCIDRs(cidrs []*net.IPNet) {
sort.Slice(cidrs, func(i, j int) bool {
return cidrs[i].String() < cidrs[j].String()
})
}
func writeAllowedIPs(buf *bytes.Buffer, ais []*net.IPNet) error {
if len(ais) == 0 {
return nil
}
var err error
if err = writeKey(buf, allowedIPsKey); err != nil {
return err
}
for i := range ais {
if i != 0 {
if _, err = buf.WriteString(", "); err != nil {
return err
}
}
if _, err = buf.WriteString(ais[i].String()); err != nil {
return err
}
}
if err = buf.WriteByte('\n'); err != nil {
return err
}
return nil
}
func writePKey(buf *bytes.Buffer, k key, b []byte) error {
if len(b) == 0 {
return nil
}
var err error
if err = writeKey(buf, k); err != nil {
return err
}
if _, err = buf.Write(b); err != nil {
return err
}
if err = buf.WriteByte('\n'); err != nil {
return err
}
return nil
}
func writeValue(buf *bytes.Buffer, k key, v string) error {
var err error
if err = writeKey(buf, k); err != nil {
return err
}
if _, err = buf.WriteString(v); err != nil {
return err
}
if err = buf.WriteByte('\n'); err != nil {
return err
}
return nil
}
func writeEndpoint(buf *bytes.Buffer, e *Endpoint) error {
if e == nil {
return nil
}
var err error
if err = writeKey(buf, endpointKey); err != nil {
return err
}
if _, err = buf.WriteString(e.IP.String()); err != nil {
return err
}
if err = buf.WriteByte(':'); err != nil {
return err
}
if _, err = buf.WriteString(strconv.FormatUint(uint64(e.Port), 10)); err != nil {
return err
}
if err = buf.WriteByte('\n'); err != nil {
return err
}
return nil
}
func writeSection(buf *bytes.Buffer, s section) error {
var err error
if err = buf.WriteByte('['); err != nil {
return err
}
if _, err = buf.WriteString(string(s)); err != nil {
return err
}
if err = buf.WriteByte(']'); err != nil {
return err
}
if err = buf.WriteByte('\n'); err != nil {
return err
}
return nil
}
func writeKey(buf *bytes.Buffer, k key) error {
var err error
if _, err = buf.WriteString(string(k)); err != nil {
return err
}
if _, err = buf.WriteString(" = "); err != nil {
return err
}
return nil
}

View File

@ -19,11 +19,9 @@ import (
"fmt"
"os/exec"
"regexp"
"sort"
"strconv"
"github.com/vishvananda/netlink"
"gopkg.in/ini.v1"
)
type wgLink struct {
@ -84,7 +82,8 @@ func Keys() ([]byte, []byte, error) {
// GenKey generates a WireGuard private key.
func GenKey() ([]byte, error) {
return exec.Command("wg", "genkey").Output()
key, err := exec.Command("wg", "genkey").Output()
return bytes.Trim(key, "\n"), err
}
// PubKey generates a WireGuard public key for a given private key.
@ -104,7 +103,7 @@ func PubKey(key []byte) ([]byte, error) {
if err != nil {
return nil, fmt.Errorf("failed to generate public key: %v", err)
}
return public, nil
return bytes.Trim(public, "\n"), nil
}
// SetConf applies a WireGuard configuration file to the given interface.
@ -129,55 +128,3 @@ func ShowConf(iface string) ([]byte, error) {
}
return stdout.Bytes(), nil
}
// CompareConf compares two WireGuard configurations.
// It returns true if they are equal, false if they are not,
// and any error that was encountered.
// Note: CompareConf only goes one level deep, as WireGuard
// configurations are not nested further than that.
func CompareConf(a, b []byte) (bool, error) {
iniA, err := ini.Load(a)
if err != nil {
return false, fmt.Errorf("failed to parse configuration: %v", err)
}
iniB, err := ini.Load(b)
if err != nil {
return false, fmt.Errorf("failed to parse configuration: %v", err)
}
secsA, secsB := iniA.SectionStrings(), iniB.SectionStrings()
if len(secsA) != len(secsB) {
return false, nil
}
sort.Strings(secsA)
sort.Strings(secsB)
var keysA, keysB []string
var valsA, valsB []string
for i := range secsA {
if secsA[i] != secsB[i] {
return false, nil
}
keysA, keysB = iniA.Section(secsA[i]).KeyStrings(), iniB.Section(secsB[i]).KeyStrings()
if len(keysA) != len(keysB) {
return false, nil
}
sort.Strings(keysA)
sort.Strings(keysB)
for j := range keysA {
if keysA[j] != keysB[j] {
return false, nil
}
valsA, valsB = iniA.Section(secsA[i]).Key(keysA[j]).Strings(","), iniB.Section(secsB[i]).Key(keysB[j]).Strings(",")
if len(valsA) != len(valsB) {
return false, nil
}
sort.Strings(valsA)
sort.Strings(valsB)
for k := range valsA {
if valsA[k] != valsB[k] {
return false, nil
}
}
}
}
return true, nil
}

View File

@ -34,108 +34,137 @@ func TestCompareConf(t *testing.T) {
{
name: "key and value order",
a: []byte(`[Interface]
PrivateKey = private
ListenPort = 51820
PrivateKey = private
ListenPort = 51820
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
b: []byte(`[Interface]
ListenPort = 51820
PrivateKey = private
ListenPort = 51820
PrivateKey = private
[Peer]
PublicKey = key
AllowedIPs = 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32, 10.2.2.0/24
Endpoint = 10.1.0.2:51820
`),
[Peer]
PublicKey = key
AllowedIPs = 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32, 10.2.2.0/24
Endpoint = 10.1.0.2:51820
`),
out: true,
},
{
name: "whitespace",
a: []byte(`[Interface]
PrivateKey = private
ListenPort = 51820
PrivateKey = private
ListenPort = 51820
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
b: []byte(`[Interface]
PrivateKey=private
ListenPort=51820
[Peer]
Endpoint=10.1.0.2:51820
PublicKey=key
AllowedIPs=10.2.2.0/24,192.168.0.1/32,10.2.3.0/24,192.168.0.2/32,10.4.0.2/32
`),
PrivateKey=private
ListenPort=51820
[Peer]
Endpoint=10.1.0.2:51820
PublicKey=key
AllowedIPs=10.2.2.0/24,192.168.0.1/32,10.2.3.0/24,192.168.0.2/32,10.4.0.2/32
`),
out: true,
},
{
name: "missing key",
a: []byte(`[Interface]
PrivateKey = private
ListenPort = 51820
PrivateKey = private
ListenPort = 51820
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
b: []byte(`[Interface]
PrivateKey = private
ListenPort = 51820
PrivateKey = private
ListenPort = 51820
[Peer]
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
[Peer]
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
out: false,
},
{
name: "section order",
a: []byte(`[Interface]
PrivateKey = private
ListenPort = 51820
PrivateKey = private
ListenPort = 51820
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
b: []byte(`[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
Endpoint = 10.1.0.2:51820
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
[Interface]
PrivateKey = private
ListenPort = 51820
`),
[Interface]
PrivateKey = private
ListenPort = 51820
`),
out: true,
},
{
name: "out of order peers",
a: []byte(`[Interface]
PrivateKey = private
ListenPort = 51820
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key2
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key1
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
b: []byte(`[Interface]
PrivateKey = private
ListenPort = 51820
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key1
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key2
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
out: true,
},
{
name: "one empty",
a: []byte(`[Interface]
PrivateKey = private
ListenPort = 51820
PrivateKey = private
ListenPort = 51820
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
[Peer]
Endpoint = 10.1.0.2:51820
PublicKey = key
AllowedIPs = 10.2.2.0/24, 192.168.0.1/32, 10.2.3.0/24, 192.168.0.2/32, 10.4.0.2/32
`),
b: []byte(``),
out: false,
},
} {
equal, err := CompareConf(tc.a, tc.b)
if err != nil {
t.Errorf("test case %q: got unexpected error: %v", tc.name, err)
}
equal := Parse(tc.a).Equal(Parse(tc.b))
if equal != tc.out {
t.Errorf("test case %q: expected %t, got %t", tc.name, tc.out, equal)
}