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: customize the krelay-server pod #58

Merged
merged 4 commits into from
Sep 3, 2024
Merged
Changes from all 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
5 changes: 4 additions & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@ run:
linters:
enable:
- dupl
- exportloopref
- copyloopvar
- gochecknoinits
- goconst
- gocritic
@@ -21,6 +21,9 @@ linters:
- usestdlibvars
- whitespace
linters-settings:
govet:
enable:
- nilness
issues:
exclude-rules:
- path: _test.go
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -60,6 +60,20 @@ EOF
$ kubectl relay -f targets.txt
```

### Customize the forwarding server

You can provide a merge patch in JSON or YAML format to customize the forwarding server. For instance:
```bash
$ cat patch.yaml
metadata:
generateName: foo-
spec:
nodeSelector:
your-key: your-value

$ kubectl --patch-file patch.yaml svc/nginx 8080:80
```

## Installation

| Distribution | Command / Link |
@@ -107,18 +121,20 @@ kubectl relay host/redis.cn-north-1.cache.amazonaws.com 6379
# Listen on port 5000 and 6000 locally, forwarding data to "1.2.3.4:5000" and "1.2.3.4:6000" from the cluster
kubectl relay ip/1.2.3.4 5000@tcp 6000@udp

# Create the agent in the kube-public namespace, and forward local port 5000 to "1.2.3.4:5000"
kubectl relay --server.namespace kube-public ip/1.2.3.4 5000
# Customized the server, and forward local port 5000 to "1.2.3.4:5000"
kubectl relay --patch '{"metadata":{"namespace":"kube-public"},"spec":{"nodeSelector":{"k": "v"}}}' ip/1.2.3.4 5000

```

## Flags

| flag | default | description |
|----------------------|-----------------------------------------|-------------------------------------------------------------|
| `--address` | `127.0.0.1` | Address to listen on. Only accepts IP addresses as a value. |
| `-f`/`--file` | N/A | Forward traffic to the targets specified in the given file. |
| `--server.image` | `ghcr.io/knight42/krelay-server:v0.0.1` | The krelay-server image to use. |
| `--server.namespace` | `default` | The namespace in which krelay-server is located. |
| flag | default | description |
|------------------|-----------------------------------------|-------------------------------------------------------------------------|
| `--address` | `127.0.0.1` | Address to listen on. Only accepts IP addresses as a value. |
| `-f`/`--file` | N/A | Forward traffic to the targets specified in the given file. |
| `--server.image` | `ghcr.io/knight42/krelay-server:v0.0.1` | The krelay-server image to use. |
| `-p`/`--patch` | N/A | The merge patch to be applied to the krelay-server pod. |
| `--patch-file` | N/A | A file containing a merge patch to be applied to the krelay-server pod. |

## How It Works

97 changes: 90 additions & 7 deletions cmd/client/main.go
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ import (
"time"

"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/cli-runtime/pkg/genericclioptions"
@@ -35,13 +36,19 @@ type Options struct {

// serverImage is the image to use for the krelay-server.
serverImage string
// Deprecated
// serverNamespace is the namespace in which krelay-server is located.
serverNamespace string
// address is the address to listen on.
address string
// targetsFile is the file containing the list of targets.
targetsFile string

// patch is the literal MergePatch
patch string
// patchFile is the file containing the MergePatch
patchFile string

verbosity int
}

@@ -62,6 +69,71 @@ func setKubernetesDefaults(config *rest.Config) {
}
}

func (o *Options) newServerPod() (*corev1.Pod, error) {
origPod := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: o.serverNamespace,
GenerateName: constants.ServerName + "-",
Labels: map[string]string{
"app.kubernetes.io/name": constants.ServerName,
"app": constants.ServerName,
},
Annotations: map[string]string{
"cluster-autoscaler.kubernetes.io/safe-to-evict": "true",
},
},
Spec: corev1.PodSpec{
AutomountServiceAccountToken: toPtr(false),
EnableServiceLinks: toPtr(false),
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: toPtr(true),
},
Containers: []corev1.Container{
{
Name: constants.ServerName,
Image: o.serverImage,
ImagePullPolicy: corev1.PullAlways,
SecurityContext: &corev1.SecurityContext{
ReadOnlyRootFilesystem: toPtr(true),
AllowPrivilegeEscalation: toPtr(false),
},
},
},
TopologySpreadConstraints: []corev1.TopologySpreadConstraint{
{
MaxSkew: 1,
TopologyKey: "kubernetes.io/hostname",
WhenUnsatisfiable: corev1.ScheduleAnyway,
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": constants.ServerName,
},
},
},
},
},
}
if len(o.patch) == 0 && len(o.patchFile) == 0 {
return &origPod, nil
}

patchBytes := []byte(o.patch)
if len(o.patchFile) > 0 {
var err error
patchBytes, err = os.ReadFile(o.patchFile)
if err != nil {
return nil, fmt.Errorf("read file: %w", err)
}
}

patched, err := patchPod(patchBytes, origPod)
if err != nil {
return nil, fmt.Errorf("patch server pod: %w", err)
}

return patched, nil
}

func (o *Options) Run(ctx context.Context, args []string) error {
ns, _, err := o.getter.ToRawKubeConfigLoader().Namespace()
if err != nil {
@@ -155,18 +227,24 @@ func (o *Options) Run(ctx context.Context, args []string) error {
}
}

slog.Info("Creating krelay-server", slog.String("namespace", o.serverNamespace))
svrPodName, err := createServerPod(ctx, cs, o.serverImage, o.serverNamespace)
svrPod, err := o.newServerPod()
if err != nil {
return err
}

slog.Info("Creating krelay-server", slog.String("namespace", svrPod.Namespace))
createdPod, err := cs.CoreV1().Pods(svrPod.Namespace).Create(ctx, svrPod, metav1.CreateOptions{})
if err != nil {
return fmt.Errorf("create krelay-server pod: %w", err)
}
defer removeServerPod(cs, o.serverNamespace, svrPodName, time.Minute)
svrPodName := createdPod.Name
defer removeServerPod(cs, svrPod.Namespace, svrPodName, time.Minute)

err = ensureServerPodIsRunning(ctx, cs, o.serverNamespace, svrPodName)
err = ensureServerPodIsRunning(ctx, cs, svrPod.Namespace, svrPodName)
if err != nil {
return fmt.Errorf("ensure krelay-server is running: %w", err)
}
slog.Info("krelay-server is running", slog.String("pod", svrPodName), slog.String("namespace", o.serverNamespace))
slog.Info("krelay-server is running", slog.String("pod", svrPodName), slog.String("namespace", svrPod.Namespace))

transport, upgrader, err := spdy.RoundTripperFor(restCfg)
if err != nil {
@@ -180,7 +258,7 @@ func (o *Options) Run(ctx context.Context, args []string) error {

req := restClient.Post().
Resource("pods").
Namespace(o.serverNamespace).Name(svrPodName).
Namespace(svrPod.Namespace).Name(svrPodName).
SubResource("portforward")
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, http.MethodPost, req.URL())
streamConn, _, err := dialer.Dial(constants.PortForwardProtocolV1Name)
@@ -255,8 +333,13 @@ service, ip and hostname rather than only pods.`,
flags.BoolVarP(&printVersion, "version", "V", false, "Print version info and exit.")
flags.StringVar(&o.address, "address", "127.0.0.1", "Address to listen on. Only accepts IP addresses as a value.")
flags.StringVarP(&o.targetsFile, "file", "f", "", "Forward to the targets specified in the given file, with one target per line.")
flags.IntVarP(&o.verbosity, "v", "v", 3, "Number for the log level verbosity. The bigger the more verbose.")
flags.StringVarP(&o.patch, "patch", "p", "", "The merge patch to be applied to the krelay-server pod.")
flags.StringVar(&o.patchFile, "patch-file", "", "A file containing a merge patch to be applied to the krelay-server pod.")
flags.StringVar(&o.serverImage, "server.image", "ghcr.io/knight42/krelay-server:v0.0.4", "The krelay-server image to use.")

flags.StringVar(&o.serverNamespace, "server.namespace", metav1.NamespaceDefault, "The namespace in which krelay-server is located.")
flags.IntVarP(&o.verbosity, "v", "v", 3, "Number for the log level verbosity. The bigger the more verbose.")
_ = flags.MarkDeprecated("server.namespace", "please use --patch/--patch-file instead.")
_ = flags.MarkHidden("server.namespace")
_ = c.Execute()
}
4 changes: 2 additions & 2 deletions cmd/client/template.go
Original file line number Diff line number Diff line change
@@ -32,8 +32,8 @@ func example() string {
# Listen on port 5000 and 6000 locally, forwarding data to "1.2.3.4:5000" and "1.2.3.4:6000" from the cluster
{{.Name}} ip/1.2.3.4 5000@tcp 6000@udp
# Create the agent in the kube-public namespace, and forward local port 5000 to "1.2.3.4:5000"
{{.Name}} --server.namespace kube-public ip/1.2.3.4 5000
# Customize the server, and forward local port 5000 to "1.2.3.4:5000"
{{.Name}} --patch '{"metadata":{"namespace":"kube-public"},"spec":{"nodeSelector":{"k": "v"}}}' ip/1.2.3.4 5000
# Forward traffic to multiple targets
echo 'ip/1.2.3.4 5000\nsvc/my-service 8080:80\n-n kube-system deploy/coredns 5353:53@udp' | {{.Name}} -f -
70 changes: 24 additions & 46 deletions cmd/client/utils.go
Original file line number Diff line number Diff line change
@@ -15,13 +15,16 @@ import (
"strings"
"time"

jsonpatch "github.com/evanphx/json-patch/v5"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
k8serr "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/httpstream"
"k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/kubernetes"

@@ -35,54 +38,29 @@ func toPtr[T any](v T) *T {
return &v
}

func createServerPod(ctx context.Context, cs kubernetes.Interface, svrImg, namespace string) (string, error) {
pod, err := cs.CoreV1().Pods(namespace).Create(ctx, &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
GenerateName: constants.ServerName + "-",
Labels: map[string]string{
"app.kubernetes.io/name": constants.ServerName,
"app": constants.ServerName,
},
Annotations: map[string]string{
"cluster-autoscaler.kubernetes.io/safe-to-evict": "true",
},
},
Spec: corev1.PodSpec{
AutomountServiceAccountToken: toPtr(false),
EnableServiceLinks: toPtr(false),
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: toPtr(true),
},
Containers: []corev1.Container{
{
Name: constants.ServerName,
Image: svrImg,
ImagePullPolicy: corev1.PullAlways,
SecurityContext: &corev1.SecurityContext{
ReadOnlyRootFilesystem: toPtr(true),
AllowPrivilegeEscalation: toPtr(false),
},
},
},
TopologySpreadConstraints: []corev1.TopologySpreadConstraint{
{
MaxSkew: 1,
TopologyKey: "kubernetes.io/hostname",
WhenUnsatisfiable: corev1.ScheduleAnyway,
LabelSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": constants.ServerName,
},
},
},
},
},
}, metav1.CreateOptions{})
func patchPod(patchBytes []byte, origPod corev1.Pod) (*corev1.Pod, error) {
patchJSONBytes, err := yaml.ToJSON(patchBytes)
if err != nil {
return "", err
return nil, fmt.Errorf("convert patch to json: %w", err)
}
return pod.Name, nil

origBytes, err := json.Marshal(origPod)
if err != nil {
return nil, fmt.Errorf("marshal pod: %w", err)
}

after, err := jsonpatch.MergePatch(origBytes, patchJSONBytes)
if err != nil {
return nil, fmt.Errorf("apply merge patch: %w", err)
}

var patchedPod corev1.Pod
err = json.Unmarshal(after, &patchedPod)
if err != nil {
return nil, fmt.Errorf("unmarshal pod: %w", err)
}

return &patchedPod, nil
}

func ensureServerPodIsRunning(ctx context.Context, cs kubernetes.Interface, namespace, podName string) error {
54 changes: 54 additions & 0 deletions cmd/client/utils_test.go
Original file line number Diff line number Diff line change
@@ -5,6 +5,8 @@ import (
"testing"

"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestParseTargetsFile(t *testing.T) {
@@ -111,3 +113,55 @@ svc/q 8000
})
}
}

func TestPatchPod(t *testing.T) {
testCases := map[string]struct {
patch string
origPod corev1.Pod

expected corev1.Pod
}{
"patch in json": {
patch: `{"metadata": {"name": "foo"}}`,
origPod: corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
Namespace: metav1.NamespaceDefault,
},
},

expected: corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: metav1.NamespaceDefault,
},
},
},

"patch in yaml": {
patch: `
metadata:
name: foo`,
origPod: corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "bar",
Namespace: metav1.NamespaceDefault,
},
},

expected: corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: metav1.NamespaceDefault,
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
got, err := patchPod([]byte(tc.patch), tc.origPod)
require.NoError(t, err)
require.EqualValues(t, tc.expected, *got)
})
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ module github.com/knight42/krelay
go 1.22.0

require (
github.com/evanphx/json-patch/v5 v5.9.0
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
k8s.io/api v0.31.0
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -23,6 +23,8 @@ github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxER
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=