Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add egctl ratelimit config support #2674

Merged
merged 3 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions internal/cmd/egctl/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@
)

const (
adminPort = 19000 // TODO: make this configurable until EG support
containerName = "envoy" // TODO: make this configurable until EG support
adminPort = 19000 // TODO: make this configurable until EG support
rateLimitDebugPort = 6070 // TODO: make this configurable until EG support
containerName = "envoy" // TODO: make this configurable until EG support
)

type aggregatedConfigDump map[string]map[string]protoreflect.ProtoMessage
Expand Down Expand Up @@ -79,7 +80,7 @@
for _, pod := range pods {
pod := pod
go func() {
fw, err := portForwarder(cli, pod)
fw, err := portForwarder(cli, pod, adminPort)

Check warning on line 83 in internal/cmd/egctl/config.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config.go#L83

Added line #L83 was not covered by tests
if err != nil {
errs = errors.Join(errs, err)
return
Expand Down Expand Up @@ -180,8 +181,8 @@
}

// portForwarder returns a port forwarder instance for a single Pod.
func portForwarder(cli kube.CLIClient, nn types.NamespacedName) (kube.PortForwarder, error) {
fw, err := kube.NewLocalPortForwarder(cli, nn, 0, adminPort)
func portForwarder(cli kube.CLIClient, nn types.NamespacedName, port int) (kube.PortForwarder, error) {
fw, err := kube.NewLocalPortForwarder(cli, nn, 0, port)
if err != nil {
return nil, err
}
Expand Down
5 changes: 5 additions & 0 deletions internal/cmd/egctl/config_cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
}

cfgCommand.AddCommand(proxyCommand())
cfgCommand.AddCommand(ratelimitCommand())

Check warning on line 26 in internal/cmd/egctl/config_cmd.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_cmd.go#L26

Added line #L26 was not covered by tests

flags := cfgCommand.Flags()
options.AddKubeConfigFlags(flags)
Expand All @@ -35,6 +36,10 @@
return cfgCommand
}

func ratelimitCommand() *cobra.Command {
Copy link
Member

Choose a reason for hiding this comment

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

why not use ratelimitConfigCommand directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have thought about this problem, but I hope to write it like the func proxyCommand() function for backward compatibility.

return ratelimitConfigCommand()

Check warning on line 40 in internal/cmd/egctl/config_cmd.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_cmd.go#L39-L40

Added lines #L39 - L40 were not covered by tests
}

func proxyCommand() *cobra.Command {
c := &cobra.Command{
Use: "envoy-proxy",
Expand Down
206 changes: 206 additions & 0 deletions internal/cmd/egctl/config_ratelimit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// Copyright Envoy Gateway Authors
// SPDX-License-Identifier: Apache-2.0
// The full text of the Apache license is available in the LICENSE file at
// the root of the repo.

package egctl

import (
"context"
"errors"
"fmt"
"io"
"net/http"

Check failure on line 14 in internal/cmd/egctl/config_ratelimit.go

View workflow job for this annotation

GitHub Actions / lint

File is not `goimports`-ed with -local github.com/envoyproxy/gateway/ (goimports)
"github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/internal/envoygateway"
"github.com/envoyproxy/gateway/internal/infrastructure/kubernetes/ratelimit"
"github.com/envoyproxy/gateway/internal/kubernetes"
ShyunnY marked this conversation as resolved.
Show resolved Hide resolved
"github.com/spf13/cobra"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/types"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
cmdutil "k8s.io/kubectl/pkg/cmd/util"
)

var (
defaultRateLimitNamespace = "envoy-gateway-system" // TODO: make this configurable until EG support
defaultConfigMap = "envoy-gateway-config" // TODO: make this configurable until EG support
defaultConfigMapKey = "envoy-gateway.yaml" // TODO: make this configurable until EG support
)

func ratelimitConfigCommand() *cobra.Command {

var (
namespace string
)

rlConfigCmd := &cobra.Command{
Use: "envoy-ratelimit",
Aliases: []string{"rl"},
Long: `Retrieve the relevant rate limit configuration from the Rate Limit instance`,
Example: ` # Retrieve rate limit configuration
egctl config ratelimit
ShyunnY marked this conversation as resolved.
Show resolved Hide resolved

# Retrieve rate limit configuration with short syntax
egctl c rl
`,
Run: func(c *cobra.Command, args []string) {
cmdutil.CheckErr(runRateLimitConfig(c, namespace))
},

Check warning on line 51 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L33-L51

Added lines #L33 - L51 were not covered by tests
}

rlConfigCmd.Flags().StringVarP(&namespace, "namespace", "n", defaultRateLimitNamespace, "Specific a namespace to get resources")
return rlConfigCmd

Check warning on line 55 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L54-L55

Added lines #L54 - L55 were not covered by tests
}

func runRateLimitConfig(c *cobra.Command, ns string) error {

cli, err := getCLIClient()
if err != nil {
return err
}

Check warning on line 63 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L58-L63

Added lines #L58 - L63 were not covered by tests

out, err := retrieveRateLimitConfig(cli, ns)
if err != nil {
return err
}

Check warning on line 68 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L65-L68

Added lines #L65 - L68 were not covered by tests

_, err = fmt.Fprintln(c.OutOrStdout(), string(out))
return err

Check warning on line 71 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L70-L71

Added lines #L70 - L71 were not covered by tests
}

func retrieveRateLimitConfig(cli kubernetes.CLIClient, ns string) ([]byte, error) {

// Before retrieving the rate limit configuration
// we make sure that the global rate limit feature is enabled
if enable, err := checkEnableGlobalRateLimit(cli); !enable {
return nil, fmt.Errorf("global rate limit feature is not enabled")
} else if err != nil {
return nil, fmt.Errorf("failed to get global rate limit status: %w", err)
}

Check warning on line 82 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L74-L82

Added lines #L74 - L82 were not covered by tests

// Filter out all rate limit pods in the Running state
rlNN, err := fetchRunningRateLimitPods(cli, ns, ratelimit.RateLimitLabelSelector())
if err != nil {
return nil, err
}

Check warning on line 88 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L85-L88

Added lines #L85 - L88 were not covered by tests

// In fact, the configuration of multiple rate limit replicas are the same.
// After we filter out the rate limit Pods in the Running state,
// we can directly use the first pod.
rlPod := rlNN[0]
fw, err := portForwarder(cli, rlPod, rateLimitDebugPort)
if err != nil {
return nil, fmt.Errorf("failed to initialize pod-forwarding for %s/%s: %w", rlPod.Namespace, rlPod.Name, err)
}

Check warning on line 97 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L93-L97

Added lines #L93 - L97 were not covered by tests

return extractRateLimitConfig(fw, rlPod)

Check warning on line 99 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L99

Added line #L99 was not covered by tests
}

// fetchRunningRateLimitPods gets the rate limit Pods, based on the labelSelectors.
// It further filters out only those rate limit Pods that are in "Running" state.
func fetchRunningRateLimitPods(cli kubernetes.CLIClient, namespace string, labelSelector []string) ([]types.NamespacedName, error) {

// Since multiple replicas of the rate limit are configured to be equal,
// we do not need to use the pod name to obtain the specified pod.
rlPods, err := cli.PodsForSelector(namespace, labelSelector...)
if err != nil {
return nil, err
}

Check warning on line 111 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L110-L111

Added lines #L110 - L111 were not covered by tests

rlNN := []types.NamespacedName{}
for _, rlPod := range rlPods.Items {
rlPodNsName := types.NamespacedName{
Namespace: rlPod.Namespace,
Name: rlPod.Name,
}
if rlPod.Status.Phase != "Running" {
ShyunnY marked this conversation as resolved.
Show resolved Hide resolved
continue
}

rlNN = append(rlNN, rlPodNsName)
}
if len(rlNN) == 0 {
return nil, fmt.Errorf("please check that the rate limit instance starts properly")
}

return rlNN, nil
}

// extractRateLimitConfig After turning on port forwarding through PortForwarder,
// construct a request and send it to the rate limit Pod to obtain relevant configuration information.
func extractRateLimitConfig(fw kubernetes.PortForwarder, rlPod types.NamespacedName) ([]byte, error) {

if err := fw.Start(); err != nil {
return nil, fmt.Errorf("failed to start port forwarding for pod %s/%s: %w", rlPod.Namespace, rlPod.Name, err)
}

Check warning on line 138 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L137-L138

Added lines #L137 - L138 were not covered by tests
defer fw.Stop()

out, err := rateLimitConfigRequest(fw.Address())
if err != nil {
return nil, fmt.Errorf("failed to send request to get rate config for pod %s/%s: %w", rlPod.Namespace, rlPod.Name, err)
}

Check warning on line 144 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L143-L144

Added lines #L143 - L144 were not covered by tests

return out, nil
}

// checkEnableGlobalRateLimit Check whether the Global Rate Limit function is enabled
func checkEnableGlobalRateLimit(cli kubernetes.CLIClient) (bool, error) {

kubeCli := cli.Kube()
cm, err := kubeCli.CoreV1().
ConfigMaps(defaultRateLimitNamespace).
Get(context.TODO(), defaultConfigMap, metav1.GetOptions{})
if err != nil {
return false, err
}

Check warning on line 158 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L157-L158

Added lines #L157 - L158 were not covered by tests

config, ok := cm.Data[defaultConfigMapKey]
if !ok {
return false, fmt.Errorf("failed to get envoy-gateway configuration")
}

Check warning on line 163 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L162-L163

Added lines #L162 - L163 were not covered by tests

decoder := serializer.NewCodecFactory(envoygateway.GetScheme()).UniversalDeserializer()
obj, gvk, err := decoder.Decode([]byte(config), nil, nil)
if err != nil {
return false, err
}

Check warning on line 169 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L168-L169

Added lines #L168 - L169 were not covered by tests

if gvk.Group != v1alpha1.GroupVersion.Group ||
gvk.Version != v1alpha1.GroupVersion.Version ||
gvk.Kind != v1alpha1.KindEnvoyGateway {
return false, errors.New("failed to decode unmatched resource type")
}

Check warning on line 175 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L174-L175

Added lines #L174 - L175 were not covered by tests

eg, ok := obj.(*v1alpha1.EnvoyGateway)
if !ok {
return false, errors.New("failed to convert object to EnvoyGateway type")
}

Check warning on line 180 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L179-L180

Added lines #L179 - L180 were not covered by tests

if eg.RateLimit == nil || eg.RateLimit.Backend.Redis == nil {
return false, nil
}

return true, nil
}

func rateLimitConfigRequest(address string) ([]byte, error) {
url := fmt.Sprintf("http://%s/rlconfig", address)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}

Check warning on line 195 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L194-L195

Added lines #L194 - L195 were not covered by tests

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}

Check warning on line 200 in internal/cmd/egctl/config_ratelimit.go

View check run for this annotation

Codecov / codecov/patch

internal/cmd/egctl/config_ratelimit.go#L199-L200

Added lines #L199 - L200 were not covered by tests
defer func() {
_ = resp.Body.Close()
}()

return io.ReadAll(resp.Body)
}
Loading
Loading