diff --git a/.travis.yml b/.travis.yml index c6978aebc..fb4d9391b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,10 @@ env: - GO111MODULE=on before_script: - go get golang.org/x/lint/golint + - docker pull lkdevelopment/hetzner-cloud-api-mock + - docker run -d -p 127.0.0.1:4000:8080 lkdevelopment/hetzner-cloud-api-mock + - docker ps -a + - sleep 10 script: - make test - make diff --git a/deploy/development-networks.yaml b/deploy/development-networks.yaml new file mode 100644 index 000000000..c54328c5f --- /dev/null +++ b/deploy/development-networks.yaml @@ -0,0 +1,81 @@ +# NOTE: this release was tested against kubernetes v1.15.x +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: cloud-controller-manager + namespace: kube-system +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1beta1 +metadata: + name: system:cloud-controller-manager +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: cluster-admin +subjects: + - kind: ServiceAccount + name: cloud-controller-manager + namespace: kube-system +--- +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: hcloud-cloud-controller-manager + namespace: kube-system +spec: + replicas: 1 + revisionHistoryLimit: 2 + template: + metadata: + labels: + app: hcloud-cloud-controller-manager + annotations: + scheduler.alpha.kubernetes.io/critical-pod: '' + spec: + serviceAccount: cloud-controller-manager + dnsPolicy: Default + tolerations: + # this taint is set by all kubelets running `--cloud-provider=external` + # so we should tolerate it to schedule the cloud controller manager + - key: "node.cloudprovider.kubernetes.io/uninitialized" + value: "true" + effect: "NoSchedule" + - key: "CriticalAddonsOnly" + operator: "Exists" + # cloud controller manages should be able to run on masters + - key: "node-role.kubernetes.io/master" + effect: NoSchedule + - key: "node.kubernetes.io/not-ready" + effect: "NoSchedule" + hostNetwork: true + containers: + - image: hetznercloud/hcloud-cloud-controller-manager:v1.4.0-b1 + name: hcloud-cloud-controller-manager + command: + - "/bin/hcloud-cloud-controller-manager" + - "--cloud-provider=hcloud" + - "--leader-elect=false" + - "--allow-untagged-cloud" + - "--allocate-node-cidrs=true" + - "--cluster-cidr=10.244.0.0/16" + resources: + requests: + cpu: 100m + memory: 50Mi + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: HCLOUD_TOKEN + valueFrom: + secretKeyRef: + name: hcloud + key: token + - name: HCLOUD_NETWORK + valueFrom: + secretKeyRef: + name: hcloud + key: network \ No newline at end of file diff --git a/deploy/development.yaml b/deploy/development.yaml index ae6edbd57..145ffd1dc 100644 --- a/deploy/development.yaml +++ b/deploy/development.yaml @@ -1,4 +1,4 @@ -# NOTE: this release was tested against kubernetes v1.9.x - 1.12.x +# NOTE: this release was tested against kubernetes v1.15.x --- apiVersion: v1 @@ -16,9 +16,9 @@ roleRef: kind: ClusterRole name: cluster-admin subjects: -- kind: ServiceAccount - name: cloud-controller-manager - namespace: kube-system + - kind: ServiceAccount + name: cloud-controller-manager + namespace: kube-system --- apiVersion: extensions/v1beta1 kind: Deployment @@ -38,35 +38,42 @@ spec: serviceAccount: cloud-controller-manager dnsPolicy: Default tolerations: - # this taint is set by all kubelets running `--cloud-provider=external` - # so we should tolerate it to schedule the cloud controller manager - - key: "node.cloudprovider.kubernetes.io/uninitialized" - value: "true" - effect: "NoSchedule" - - key: "CriticalAddonsOnly" - operator: "Exists" - # cloud controller manages should be able to run on masters - - key: "node-role.kubernetes.io/master" - effect: NoSchedule + # this taint is set by all kubelets running `--cloud-provider=external` + # so we should tolerate it to schedule the cloud controller manager + - key: "node.cloudprovider.kubernetes.io/uninitialized" + value: "true" + effect: "NoSchedule" + - key: "CriticalAddonsOnly" + operator: "Exists" + # cloud controller manages should be able to run on masters + - key: "node-role.kubernetes.io/master" + effect: NoSchedule + - key: "node.kubernetes.io/not-ready" + effect: "NoSchedule" containers: - - image: hetznercloud/hcloud-cloud-controller-manager:latest - name: hcloud-cloud-controller-manager - command: - - "/bin/hcloud-cloud-controller-manager" - - "--cloud-provider=hcloud" - - "--leader-elect=false" - - "--allow-untagged-cloud" - resources: - requests: - cpu: 100m - memory: 50Mi - env: - - name: NODE_NAME - valueFrom: - fieldRef: - fieldPath: spec.nodeName - - name: HCLOUD_TOKEN - valueFrom: - secretKeyRef: - name: hcloud - key: token + - image: hetznercloud/hcloud-cloud-controller-manager:v1.4.0-b1 + name: hcloud-cloud-controller-manager + command: + - "/bin/hcloud-cloud-controller-manager" + - "--cloud-provider=hcloud" + - "--leader-elect=false" + - "--allow-untagged-cloud" + resources: + requests: + cpu: 100m + memory: 50Mi + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: HCLOUD_TOKEN + valueFrom: + secretKeyRef: + name: hcloud + key: token + - name: HCLOUD_NETWORK + valueFrom: + secretKeyRef: + name: hcloud + key: network \ No newline at end of file diff --git a/go.mod b/go.mod index a278e94c0..755ac7b1d 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.8.5 // indirect github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47 // indirect - github.com/hetznercloud/hcloud-go v1.12.0 + github.com/hetznercloud/hcloud-go v1.14.0 github.com/imdario/mergo v0.0.0-20180119215619-163f41321a19 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jonboulle/clockwork v0.1.0 // indirect @@ -61,7 +61,7 @@ require ( github.com/spf13/cobra v0.0.1 // indirect github.com/spf13/pflag v0.0.0-20171106142849-4c012f6dcd95 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect - github.com/ugorji/go v1.1.1 + github.com/ugorji/go v1.1.1 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect go.etcd.io/bbolt v1.3.2 // indirect go.uber.org/atomic v1.3.2 // indirect @@ -74,10 +74,10 @@ require ( gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/square/go-jose.v2 v2.3.0 // indirect k8s.io/api v0.0.0-20180712090710-2d6f90ab1293 - k8s.io/apiextensions-apiserver v0.0.0-20180718013825-06dfdaae5c2b + k8s.io/apiextensions-apiserver v0.0.0-20180718013825-06dfdaae5c2b // indirect k8s.io/apimachinery v0.0.0-20180621070125-103fd098999d k8s.io/apiserver v0.0.0-20180718002855-8b122ec9e3bb - k8s.io/client-go v2.0.0-alpha.0.0.20180718001006-59698c7d9724+incompatible + k8s.io/client-go v2.0.0-alpha.0.0.20180718001006-59698c7d9724+incompatible // indirect k8s.io/klog v0.2.0 // indirect k8s.io/kube-openapi v0.0.0-20181106182614-a9a16210091c // indirect k8s.io/kubernetes v1.11.1 diff --git a/go.sum b/go.sum index 76bb3cd76..59c29efd7 100644 --- a/go.sum +++ b/go.sum @@ -73,9 +73,12 @@ github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47 h1:UnszMmmmm5 github.com/hashicorp/golang-lru v0.0.0-20180201235237-0fb14efe8c47/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hetznercloud/hcloud-go v1.12.0 h1:ugZO8a8ADekqSWi7xWlcs6pxr4QE0tw5VnyjXcL5n28= github.com/hetznercloud/hcloud-go v1.12.0/go.mod h1:g5pff0YNAZywQaivY/CmhUYFVp7oP0nu3MiODC2W4Hw= +github.com/hetznercloud/hcloud-go v1.14.0 h1:6IdF0Vox/6j1pyEdUCbFPIzEH/K9xZZzVuSFro8Y2vw= +github.com/hetznercloud/hcloud-go v1.14.0/go.mod h1:8lR3yHBHZWy2uGcUi9Ibt4UOoop2wrVdERJgCtxsF3Q= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.0.0-20180119215619-163f41321a19 h1:geJOJJZwkYI1yqxWrAMcgrwDvy4P1XyNNgIyN9d6UXc= github.com/imdario/mergo v0.0.0-20180119215619-163f41321a19/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= diff --git a/hcloud/cloud.go b/hcloud/cloud.go index 42c304fd3..8bbaf6bbe 100644 --- a/hcloud/cloud.go +++ b/hcloud/cloud.go @@ -29,15 +29,18 @@ import ( const ( hcloudTokenENVVar = "HCLOUD_TOKEN" hcloudEndpointENVVar = "HCLOUD_ENDPOINT" + hcloudNetworkENVVar = "HCLOUD_NETWORK" nodeNameENVVar = "NODE_NAME" providerName = "hcloud" - providerVersion = "v1.3.0" + providerVersion = "v1.4.0" ) type cloud struct { client *hcloud.Client instances cloudprovider.Instances zones cloudprovider.Zones + routes cloudprovider.Routes + network string } func newCloud(config io.Reader) (cloudprovider.Interface, error) { @@ -50,6 +53,8 @@ func newCloud(config io.Reader) (cloudprovider.Interface, error) { return nil, fmt.Errorf("environment variable %q is required", nodeNameENVVar) } + network := os.Getenv(hcloudNetworkENVVar) + opts := []hcloud.ClientOption{ hcloud.WithToken(token), hcloud.WithApplication("hcloud-cloud-controller", providerVersion), @@ -63,6 +68,8 @@ func newCloud(config io.Reader) (cloudprovider.Interface, error) { client: client, zones: newZones(client, nodeName), instances: newInstances(client), + routes: nil, + network: network, }, nil } @@ -85,7 +92,16 @@ func (c *cloud) Clusters() (cloudprovider.Clusters, bool) { } func (c *cloud) Routes() (cloudprovider.Routes, bool) { - return nil, false + if len(c.network) > 0 { + r, err := newRoutes(c.client, c.network) + if err != nil { + fmt.Printf("%+v", err) + return nil, false + } + return r, true + } + return nil, false // If no network is configured, disable the routes part + } func (c *cloud) ProviderName() string { diff --git a/hcloud/cloud_test.go b/hcloud/cloud_test.go index 568b800e5..aeb1d0f03 100644 --- a/hcloud/cloud_test.go +++ b/hcloud/cloud_test.go @@ -67,6 +67,8 @@ func TestNewCloud(t *testing.T) { } func TestCloud(t *testing.T) { + os.Setenv("HCLOUD_TOKEN", "test") + os.Setenv("NODE_NAME", "test") var config bytes.Buffer cloud, err := newCloud(&config) if err != nil { @@ -108,6 +110,16 @@ func TestCloud(t *testing.T) { } }) + t.Run("RoutesWithNetworks", func(t *testing.T) { + os.Setenv("HCLOUD_NETWORK", "1") + os.Setenv("HCLOUD_ENDPOINT", "http://127.0.0.1:4000/v1") // We need the mock server for testing this + c, _ := newCloud(&config) + _, supported := c.Routes() + if !supported { + t.Error("Routes interface should be supported") + } + }) + t.Run("HasClusterID", func(t *testing.T) { if cloud.HasClusterID() { t.Error("HasClusterID should be false") diff --git a/hcloud/instances.go b/hcloud/instances.go index 30c0945ac..423bba58c 100644 --- a/hcloud/instances.go +++ b/hcloud/instances.go @@ -19,6 +19,7 @@ package hcloud import ( "context" "k8s.io/kubernetes/pkg/cloudprovider" + "os" "strconv" "github.com/hetznercloud/hcloud-go/hcloud" @@ -44,7 +45,7 @@ func (i *instances) NodeAddressesByProviderID(ctx context.Context, providerID st if err != nil { return nil, err } - return nodeAddresses(server), nil + return i.nodeAddresses(ctx, server), nil } func (i *instances) NodeAddresses(ctx context.Context, nodeName types.NodeName) ([]v1.NodeAddress, error) { @@ -52,7 +53,7 @@ func (i *instances) NodeAddresses(ctx context.Context, nodeName types.NodeName) if err != nil { return nil, err } - return nodeAddresses(server), nil + return i.nodeAddresses(ctx, server), nil } func (i *instances) ExternalID(ctx context.Context, nodeName types.NodeName) (string, error) { @@ -128,12 +129,27 @@ func (i instances) InstanceShutdownByProviderID(ctx context.Context, providerID return } -func nodeAddresses(server *hcloud.Server) []v1.NodeAddress { +func (i *instances) nodeAddresses(ctx context.Context, server *hcloud.Server) []v1.NodeAddress { var addresses []v1.NodeAddress addresses = append( addresses, v1.NodeAddress{Type: v1.NodeHostName, Address: server.Name}, v1.NodeAddress{Type: v1.NodeExternalIP, Address: server.PublicNet.IPv4.IP.String()}, ) + n := os.Getenv(hcloudNetworkENVVar) + if len(n) > 0 { + network, _, _ := i.client.Network.Get(ctx, n) + if network != nil { + for _, privateNet := range server.PrivateNet { + if privateNet.Network.ID == network.ID { + addresses = append( + addresses, + v1.NodeAddress{Type: v1.NodeInternalIP, Address: privateNet.IP.String()}, + ) + } + } + + } + } return addresses } diff --git a/hcloud/routes.go b/hcloud/routes.go new file mode 100644 index 000000000..a25b8cfc4 --- /dev/null +++ b/hcloud/routes.go @@ -0,0 +1,204 @@ +package hcloud + +import ( + "context" + "fmt" + "github.com/hetznercloud/hcloud-go/hcloud" + "k8s.io/apimachinery/pkg/types" + "k8s.io/kubernetes/pkg/cloudprovider" + "net" + "time" +) + +type routes struct { + client *hcloud.Client + network *hcloud.Network + serverNames map[string]string + serverIPs map[string]net.IP +} + +func newRoutes(client *hcloud.Client, network string) (*routes, error) { + networkObj, _, err := client.Network.Get(context.Background(), network) + if err != nil { + return nil, err + } + + return &routes{client, networkObj, make(map[string]string), make(map[string]net.IP)}, nil +} + +func (r *routes) reloadNetwork(ctx context.Context) error { + networkObj, _, err := r.client.Network.GetByID(context.Background(), r.network.ID) + if err != nil { + return err + } + r.network = networkObj + return nil +} + +func (r *routes) loadServers(ctx context.Context) error { + serversRaw, err := r.client.Server.All(ctx) + if err != nil { + return err + } + for _, server := range serversRaw { + for _, privateNet := range server.PrivateNet { + if privateNet.Network.ID == r.network.ID { + r.serverNames[privateNet.IP.String()] = server.Name + r.serverIPs[server.Name] = privateNet.IP + break + } + } + + } + return nil +} + +// ListRoutes lists all managed routes that belong to the specified clusterName +func (r *routes) ListRoutes(ctx context.Context, clusterName string) ([]*cloudprovider.Route, error) { + err := r.reloadNetwork(ctx) + if err != nil { + return []*cloudprovider.Route{}, err + } + err = r.loadServers(ctx) + if err != nil { + return []*cloudprovider.Route{}, err + } + var routes []*cloudprovider.Route + for _, route := range r.network.Routes { + r, err := r.hcloudRouteToRoute(route) + if err != nil { + return nil, err + } + routes = append(routes, r) + } + return routes, nil +} + +// CreateRoute creates the described managed route +// route.Name will be ignored, although the cloud-provider may use nameHint +// to create a more user-meaningful name. +func (r *routes) CreateRoute(ctx context.Context, clusterName string, nameHint string, route *cloudprovider.Route) error { + err := r.loadServers(ctx) + if err != nil { + return err + } + + ip, ok := r.serverIPs[string(route.TargetNode)] + if !ok { + return fmt.Errorf("server %v not found", route.TargetNode) + } + _, cidr, err := net.ParseCIDR(route.DestinationCIDR) + if err != nil { + return err + } + doesRouteAlreadyExist, err := r.checkIfRouteAlreadyExists(ctx, route) + if err != nil { + return err + } + if !doesRouteAlreadyExist { + opts := hcloud.NetworkAddRouteOpts{ + Route: hcloud.NetworkRoute{ + Destination: cidr, + Gateway: ip, + }, + } + action, _, err := r.client.Network.AddRoute(ctx, r.network, opts) + if err != nil { + if hcloud.IsError(err, hcloud.ErrorCodeLocked) || hcloud.IsError(err, hcloud.ErrorCodeConflict) { + time.Sleep(time.Second * 5) + return r.CreateRoute(ctx, clusterName, nameHint, route) + } + return err + } + err = r.watchAction(ctx, action) + if err != nil { + return err + } + } + return nil +} + +// DeleteRoute deletes the specified managed route +// Route should be as returned by ListRoutes +func (r *routes) DeleteRoute(ctx context.Context, clusterName string, route *cloudprovider.Route) error { + err := r.loadServers(ctx) + if err != nil { + return err + } + ip, ok := r.serverIPs[string(route.TargetNode)] + if !ok { + return fmt.Errorf("server %v not found", route.TargetNode) + } + _, cidr, err := net.ParseCIDR(route.DestinationCIDR) + if err != nil { + return err + } + opts := hcloud.NetworkDeleteRouteOpts{ + Route: hcloud.NetworkRoute{ + Destination: cidr, + Gateway: ip, + }, + } + action, _, err := r.client.Network.DeleteRoute(ctx, r.network, opts) + if err != nil { + if hcloud.IsError(err, hcloud.ErrorCodeLocked) || hcloud.IsError(err, hcloud.ErrorCodeConflict) { + time.Sleep(time.Second * 5) + return r.DeleteRoute(ctx, clusterName, route) + } + return err + } + err = r.watchAction(ctx, action) + if err != nil { + return err + } + return nil +} + +func (r *routes) hcloudRouteToRoute(route hcloud.NetworkRoute) (*cloudprovider.Route, error) { + nodeName, ok := r.serverNames[route.Gateway.String()] + if !ok { + return nil, fmt.Errorf("server with IP %v not found", route.Gateway) + } + return &cloudprovider.Route{ + DestinationCIDR: route.Destination.String(), + Name: fmt.Sprintf("%s-%s", route.Gateway.String(), route.Destination.String()), + TargetNode: types.NodeName(nodeName), + }, nil +} + +func (r *routes) watchAction(ctx context.Context, action *hcloud.Action) error { + _, errCh := r.client.Action.WatchProgress(ctx, action) + if err := <-errCh; err != nil { + return err + } + return nil +} + +func (r *routes) checkIfRouteAlreadyExists(ctx context.Context, route *cloudprovider.Route) (bool, error) { + err := r.reloadNetwork(ctx) + if err != nil { + return false, err + } + + for _, _route := range r.network.Routes { + if _route.Destination.String() == route.DestinationCIDR { + ip, ok := r.serverIPs[string(route.TargetNode)] + if !ok { + return false, fmt.Errorf("server with name %v not found", string(route.TargetNode)) + } + if !_route.Gateway.Equal(ip) { + action, _, err := r.client.Network.DeleteRoute(context.Background(), r.network, hcloud.NetworkDeleteRouteOpts{ + Route: _route, + }) + if err != nil { + return false, err + } + if r.watchAction(ctx, action) != nil { + return false, err + } + } + return true, nil + } + } + return false, nil +} diff --git a/hcloud/routes_test.go b/hcloud/routes_test.go new file mode 100644 index 000000000..fd11b18d8 --- /dev/null +++ b/hcloud/routes_test.go @@ -0,0 +1,207 @@ +package hcloud + +import ( + "context" + "encoding/json" + "github.com/hetznercloud/hcloud-go/hcloud" + "github.com/hetznercloud/hcloud-go/hcloud/schema" + "k8s.io/kubernetes/pkg/cloudprovider" + "net/http" + "testing" +) + +func TestRoutes_CreateRoute(t *testing.T) { + env := newTestEnv() + defer env.Teardown() + env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(schema.ServerListResponse{ + Servers: []schema.Server{ + { + ID: 1, + Name: "node15", + PrivateNet: []schema.ServerPrivateNet{ + { + Network: 1, + IP: "10.0.0.2", + }, + }, + }, + }, + }) + }) + env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(schema.NetworkGetResponse{ + Network: schema.Network{ + ID: 1, + Name: "network-1", + IPRange: "10.0.0.0/8", + }, + }) + }) + env.Mux.HandleFunc("/actions/1", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(schema.NetworkActionAddRouteResponse{ + Action: schema.Action{ + ID: 1, + Status: string(hcloud.ActionStatusSuccess), + Progress: 100, + }, + }) + }) + env.Mux.HandleFunc("/networks/1/actions/add_route", func(w http.ResponseWriter, r *http.Request) { + var reqBody schema.NetworkActionAddRouteRequest + if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { + t.Fatal(err) + } + if reqBody.Destination != "10.5.0.0/24" { + t.Errorf("unexpected Destination: %v", reqBody.Destination) + } + if reqBody.Gateway != "10.0.0.2" { + t.Errorf("unexpected Gateway: %v", reqBody.Gateway) + } + json.NewEncoder(w).Encode(schema.NetworkActionAddRouteResponse{ + Action: schema.Action{ + ID: 1, + Progress: 0, + Status: string(hcloud.ActionStatusRunning), + }, + }) + }) + routes, err := newRoutes(env.Client, "1") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + err = routes.CreateRoute(context.TODO(), "my-cluster", "route", &cloudprovider.Route{ + Name: "route", + TargetNode: "node15", + DestinationCIDR: "10.5.0.0/24", + }) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } +} + +func TestRoutes_ListRoutes(t *testing.T) { + env := newTestEnv() + defer env.Teardown() + env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(schema.ServerListResponse{ + Servers: []schema.Server{ + { + ID: 1, + Name: "node15", + PrivateNet: []schema.ServerPrivateNet{ + { + Network: 1, + IP: "10.0.0.2", + }, + }, + }, + }, + }) + }) + env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(schema.NetworkGetResponse{ + Network: schema.Network{ + ID: 1, + Name: "network-1", + IPRange: "10.0.0.0/8", + Routes: []schema.NetworkRoute{ + { + Destination: "10.5.0.0/24", + Gateway: "10.0.0.2", + }, + }, + }, + }) + }) + routes, err := newRoutes(env.Client, "1") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + r, err := routes.ListRoutes(context.TODO(), "my-cluster") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + if len(r) != 1 { + t.Errorf("Unexpected routes %v", len(r)) + } + if r[0].DestinationCIDR != "10.5.0.0/24" { + t.Errorf("Unexpected DestinationCIDR %v", r[0].DestinationCIDR) + } + if r[0].TargetNode != "node15" { + t.Errorf("Unexpected TargetNode %v", r[0].TargetNode) + } +} + +func TestRoutes_DeleteRoute(t *testing.T) { + env := newTestEnv() + defer env.Teardown() + env.Mux.HandleFunc("/servers", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(schema.ServerListResponse{ + Servers: []schema.Server{ + { + ID: 1, + Name: "node15", + PrivateNet: []schema.ServerPrivateNet{ + { + Network: 1, + IP: "10.0.0.2", + }, + }, + }, + }, + }) + }) + env.Mux.HandleFunc("/networks/1", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(schema.NetworkGetResponse{ + Network: schema.Network{ + ID: 1, + Name: "network-1", + IPRange: "10.0.0.0/8", + }, + }) + }) + env.Mux.HandleFunc("/actions/1", func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(schema.NetworkActionAddRouteResponse{ + Action: schema.Action{ + ID: 1, + Status: string(hcloud.ActionStatusSuccess), + Progress: 100, + }, + }) + }) + env.Mux.HandleFunc("/networks/1/actions/delete_route", func(w http.ResponseWriter, r *http.Request) { + var reqBody schema.NetworkActionDeleteRouteRequest + if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { + t.Fatal(err) + } + if reqBody.Destination != "10.5.0.0/24" { + t.Errorf("unexpected Destination: %v", reqBody.Destination) + } + if reqBody.Gateway != "10.0.0.2" { + t.Errorf("unexpected Gateway: %v", reqBody.Gateway) + } + json.NewEncoder(w).Encode(schema.NetworkActionDeleteRouteResponse{ + Action: schema.Action{ + ID: 1, + Progress: 0, + Status: string(hcloud.ActionStatusRunning), + }, + }) + }) + routes, err := newRoutes(env.Client, "1") + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + err = routes.DeleteRoute(context.TODO(), "my-cluster", &cloudprovider.Route{ + Name: "route", + TargetNode: "node15", + DestinationCIDR: "10.5.0.0/24", + }) + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } +}