Skip to content

Commit

Permalink
Merge pull request #1253 from spidernet-io/feature/moveip
Browse files Browse the repository at this point in the history
Add move(migrate) egress IP cmd
  • Loading branch information
weizhoublue authored Mar 14, 2024
2 parents 6622ec6 + ce93174 commit 3a71684
Show file tree
Hide file tree
Showing 14 changed files with 708 additions and 46 deletions.
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ endef
build_all_bin:
make build_controller_bin
make build_agent_bin
make build_egctl_bin


.PHONY: build_controller_bin
Expand All @@ -35,6 +36,11 @@ build_agent_bin: CMD_BIN_DIR := $(ROOT_DIR)/cmd/agent
build_agent_bin:
$(BUILD_BIN)

.PHONY: build_egctl_bin
build_egctl_bin: CMD_BIN_DIR := $(ROOT_DIR)/cmd/egctl
build_egctl_bin:
$(BUILD_BIN)

# ------------

define BUILD_FINAL_IMAGE
Expand Down
38 changes: 38 additions & 0 deletions cmd/egctl/cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2022 Authors of spidernet-io
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"fmt"
"os"
"path/filepath"

"github.com/spf13/cobra"
)

var binName = filepath.Base(os.Args[0])

// rootCmd represents the base command.
var rootCmd = &cobra.Command{
Use: binName,
Short: "egress gateway ctl",
Run: func(cmd *cobra.Command, args []string) {
},
}

// Execute adds all child commands to the root command sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
moveCmd.Flags().StringVarP(&egressGatewayName, "egressGatewayName", "", "", "Specify the name of the egress gateway")
moveCmd.Flags().StringVarP(&vipAddress, "vip", "", "", "Specify the VIP address to MoveEgressIP")
moveCmd.Flags().StringVarP(&targetNode, "targetNode", "", "", "Specify the name of the node to MoveEgressIP the VIP to")

rootCmd.AddCommand(vipCmd)
vipCmd.AddCommand(moveCmd)

if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
168 changes: 168 additions & 0 deletions cmd/egctl/cmd/vip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright 2022 Authors of spidernet-io
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"context"
"fmt"
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/types"
"os"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"time"

egressv1 "github.com/spidernet-io/egressgateway/pkg/k8s/apis/v1beta1"
"github.com/spidernet-io/egressgateway/pkg/schema"
)

var vipCmd = &cobra.Command{
Use: "vip",
Short: "vip resources",
Long: "vip resources with specified parameters.",
Run: func(cmd *cobra.Command, args []string) {
_ = cmd.Help()
},
}

type DisplayEgressIPData struct {
IPv4 string
IPv6 string
Node string
EgressGateway string
}

var egressGatewayName, vipAddress, targetNode string

var moveCmd = &cobra.Command{
Use: "move",
Short: "move --egressGatewayName <egress-gateway-name> --vip <vip-address> --targetNode <node-name>",
Long: `move --egressGatewayName <egress-gateway-name> --vip <vip-address> --targetNode <node-name>`,
PreRun: func(cmd *cobra.Command, args []string) {
if egressGatewayName == "" || vipAddress == "" || targetNode == "" {
fmt.Println("Error: egressGatewayName, vip address, and target node name must be specified")
os.Exit(1)
}
},
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Moving VIP %s to node %s...\n", vipAddress, targetNode)
err := MoveEgressIP(egressGatewayName, vipAddress, targetNode)
if err != nil {
cmd.PrintErr("Move failed: ", err)
os.Exit(1)
}
},
}

func MoveEgressIP(egressGatewayName string, vipAddress, targetNode string) error {
kubeConfig, err := ctrl.GetConfig()
if err != nil {
return fmt.Errorf("failed to load kubeconfig: %w", err)
}
cli, err := client.New(kubeConfig, client.Options{Scheme: schema.GetScheme()})
if err != nil {
return fmt.Errorf("failed to create client: %w", err)
}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

egw := egressv1.EgressGateway{}
err = cli.Get(ctx, types.NamespacedName{Name: egressGatewayName}, &egw)
if err != nil {
return fmt.Errorf("failed to get EgressGateway: %w", err)
}

eipPair, err := findAndRemoveEIP(&egw, vipAddress, targetNode)
if err != nil {
return err
}
if eipPair == nil {
fmt.Printf("Egress IP %s is currently on node %s, with no change.\n", vipAddress, targetNode)
return nil
}

if err := moveEIPToNode(&egw, targetNode, eipPair); err != nil {
return err
}
if err := cli.Status().Update(ctx, &egw); err != nil {
return fmt.Errorf("failed to update EgressGateway status: %w", err)
}

for _, p := range eipPair.Policies {
if p.Namespace == "" {
policy := new(egressv1.EgressClusterPolicy)
err := cli.Get(ctx, types.NamespacedName{
Namespace: p.Namespace,
Name: p.Name,
}, policy)
policy.Status.Node = targetNode
if err != nil {
return fmt.Errorf("failed to get EgressClusterPolicy status: %w", err)
}
if err := cli.Status().Update(ctx, policy); err != nil {
return fmt.Errorf("failed to update EgressClusterPolicy status: %w", err)
}
} else {
policy := new(egressv1.EgressPolicy)
err := cli.Get(ctx, types.NamespacedName{
Namespace: p.Namespace,
Name: p.Name,
}, policy)
policy.Status.Node = targetNode
if err != nil {
return fmt.Errorf("failed to get EgressPolicy status: %w", err)
}
if err := cli.Status().Update(ctx, policy); err != nil {
return fmt.Errorf("failed to update EgressPolicy status: %w", err)
}
}
}

fmt.Printf("Successfully moved VIP %s to node %s\n", vipAddress, targetNode)
return nil
}

// findAndRemoveEIP finds the EIP and removes it from the current node.
// Returns the found EIP and any error that occurs.
func findAndRemoveEIP(egw *egressv1.EgressGateway, vipAddress string, targetNode string) (*egressv1.Eips, error) {
for nodeIndex, node := range egw.Status.NodeList {
for eipIndex, eip := range node.Eips {
if node.Name == targetNode {
return nil, nil
}

if eip.IPv4 == vipAddress || eip.IPv6 == vipAddress {
foundEIP := egw.Status.NodeList[nodeIndex].Eips[eipIndex]
egw.Status.NodeList[nodeIndex].Eips = append(
egw.Status.NodeList[nodeIndex].Eips[:eipIndex],
egw.Status.NodeList[nodeIndex].Eips[eipIndex+1:]...,
)
return &foundEIP, nil
}
}
}
return nil, fmt.Errorf("VIP %s not found in any node of EgressGateway", vipAddress)
}

// moveEIPToNode MoveEgressIP an EIP to the target node within the EgressGateway.
// Returns any error that occurs.
func moveEIPToNode(egw *egressv1.EgressGateway, targetNode string, eipPair *egressv1.Eips) error {
found := false
for i, node := range egw.Status.NodeList {
if node.Name == targetNode {
if string(egressv1.EgressTunnelReady) != node.Status {
return fmt.Errorf("target node '%s' not ready, please select a ready node", targetNode)
}

egw.Status.NodeList[i].Eips = append(node.Eips, *eipPair)
found = true
break
}
}
if !found {
return fmt.Errorf("target node '%s' not found in EgressGateway", targetNode)
}
return nil
}
12 changes: 12 additions & 0 deletions cmd/egctl/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright 2022 Authors of spidernet-io
// SPDX-License-Identifier: Apache-2.0

package main

import (
"github.com/spidernet-io/egressgateway/cmd/egctl/cmd"
)

func main() {
cmd.Execute()
}
2 changes: 2 additions & 0 deletions docs/mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ nav:
- Namespace Default EgressGateway: usage/NamespaceDefaultEgressGateway.md
- Cluster Default EgressGateway: usage/ClusterDefaultEgressGateway.md
- Failover: usage/EgressGatewayFailover.md
- Move EgressIP: usage/MoveIP.md
- Concepts:
- Architecture: concepts/Architecture.md
- Datapath: concepts/Datapath.md
Expand All @@ -98,6 +99,7 @@ nav:
- CRD EgressEndpointSlice: reference/EgressEndpointSlice.md
- CRD EgressClusterEndpointSlice: reference/EgressClusterEndpointSlice.md
- CRD EgressClusterInfo: reference/EgressClusterInfo.md
- egctl cli: reference/egctl.md
- Troubleshooting: Troubleshooting.md
- Development:
- DataFlow: develop/Dataflow.md
Expand Down
17 changes: 17 additions & 0 deletions docs/reference/egctl.en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# egctl cli reference

`egctl` is a command line tool for managing EgressGateway related resources.

## Command Overview

### vip move

Move a VIP to a specified node.

* `--egressGatewayName`: Specifies the name of the EgressGateway.
* `--vip`: The Egress IP address you want to move.
* `--targetNode`: The name of the target EgressGateway Node where the Egress IP will take effect.

```shell
egctl vip move --egressGatewayName <egress-gateway-name> --vip <vip-address> --targetNode <node-name>
```
17 changes: 17 additions & 0 deletions docs/reference/egctl.zh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# egctl 命令行工具说明

`egctl` 是一个命令行工具,用于管理 EgressGateway 相关资源。

## 命令概述

### vip move

移动 VIP 到指定的节点。

* `--egressGatewayName`: 指定 EgressGateway 的名称。
* `--vip`: 您想要移动的 Egress IP 地址。
* `--targetNode`: Egress IP 将生效的目标 EgressGateway 节点的名称。

```shell
egctl vip move --egressGatewayName <egress-gateway-name> --vip <vip-address> --targetNode <node-name>
```
69 changes: 69 additions & 0 deletions docs/usage/MoveIP.en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Migration of Egress IP Between Gateway Nodes

## Use Cases

* With EgressGateway, we can select multiple Nodes as EgressNodes. When a Node requires maintenance, we can manually migrate the VIP of that Node to another Node using CLI commands.
* For other reasons, when it's necessary to manually move a Node's VIP to another Node.

## Steps for Use

We examine the definition of EgressGateway by executing `kubectl get egw egressgateway -o yaml`.

```yaml
apiVersion: egressgateway.spidernet.io/v1beta1
kind: EgressGateway
metadata:
finalizers:
- egressgateway.spidernet.io/egressgateway
name: egressgateway
spec:
ippools:
ipv4:
- 10.6.91.1-10.6.93.125
ipv4DefaultEIP: 10.6.92.222
nodeSelector:
selector:
matchLabels:
egress: "true"
status:
ipUsage:
ipv4Free: 37
ipv4Total: 637
ipv6Free: 0
ipv6Total: 0
nodeList:
- name: workstation2
status: Ready
- name: workstation3
status: Ready
eips:
- ipv4: 10.6.92.209
policies:
- name: policy-1
namespace: default
```
Before the migration, the Egress IP was on the workstation2 node.
```shell
node@workstation:~$ kubectl get egp
NAME GATEWAY IPV4 IPV6 EGRESS NODE
policy-1 egressgateway 10.6.92.209 workstation3
```

We migrate the Egress IP of `workstation3` to the `workstation2` Node by executing the command below.

```log
kubectl exec -it egressgateway-controller-86c84f4858-b6dz4 bash
egctl vip move --egressGatewayName egressgateway --vip 10.6.92.209 --targetNode workstation2
Moving VIP 10.6.92.209 to node workstation2...
Successfully moved VIP 10.6.92.209 to node workstation2
```

After migration, the Egress IP node has been moved to the workstation2 node.

```shell
node@workstation:~$ kubectl get egress
NAME GATEWAY IPV4 IPV6 EGRESS NODE
policy-1 egressgateway 10.6.92.209 workstation2
```
Loading

1 comment on commit 3a71684

@weizhoublue
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.