feat: optimize code

This commit is contained in:
fengcaiwen
2023-03-16 19:11:22 +08:00
committed by wencaiwulue
parent a545f3a958
commit 1970f30f9d
13 changed files with 173 additions and 144 deletions

View File

@@ -65,7 +65,7 @@ func CmdCp(f cmdutil.Factory) *cobra.Command {
Use: "cp <file-spec-src> <file-spec-dest>", Use: "cp <file-spec-src> <file-spec-dest>",
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Short: i18n.T("Copy files and directories to and from containers"), Short: i18n.T("Copy files and directories to and from containers"),
Long: i18n.T("Copy files and directories to and from containers."), Long: i18n.T("Copy files and directories to and from containers. Different between kubectl cp is it will de-reference symbol link."),
Example: cpExample, Example: cpExample,
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
cmdutil.CheckErr(handler.SshJump(sshConf, cmd.Flags())) cmdutil.CheckErr(handler.SshJump(sshConf, cmd.Flags()))

View File

@@ -32,15 +32,22 @@ func CmdDev(f cmdutil.Factory) *cobra.Command {
Env: opts.NewListOpts(nil), Env: opts.NewListOpts(nil),
Volumes: opts.NewListOpts(nil), Volumes: opts.NewListOpts(nil),
ExtraHosts: opts.NewListOpts(nil), ExtraHosts: opts.NewListOpts(nil),
Aliases: opts.NewListOpts(nil), //Aliases: opts.NewListOpts(nil),
NoProxy: false, NoProxy: false,
ExtraCIDR: []string{}, ExtraCIDR: []string{},
} }
var sshConf = &util.SshConfig{} var sshConf = &util.SshConfig{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "dev", Use: "dev",
Short: i18n.T("Startup your workloads in local Docker container use same volume、env、and network with cluster"), Short: i18n.T("Startup your kubernetes workloads in local Docker container with same volume、env、and network"),
Long: templates.LongDesc(i18n.T(`Startup your workloads in local Docker container use same volume、env、and network with cluster`)), Long: templates.LongDesc(i18n.T(`
Startup your kubernetes workloads in local Docker container with same volume、env、and network
## What did i do:
- Download volume which MountPath point to, mount to docker container
- Connect to cluster network, set network to docker container
- Get all environment with command (env), set env to docker container
`)),
Example: templates.Examples(i18n.T(` Example: templates.Examples(i18n.T(`
# Develop workloads # Develop workloads
- develop deployment - develop deployment
@@ -150,15 +157,14 @@ func CmdDev(f cmdutil.Factory) *cobra.Command {
// docker options // docker options
cmd.Flags().Var(&devOptions.ExtraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)") cmd.Flags().Var(&devOptions.ExtraHosts, "add-host", "Add a custom host-to-IP mapping (host:ip)")
//cmd.Flags().StringVar(&devOptions.ParentContainer, "parent-container", "", "Parent container name if running in Docker (Docker in Docker)")
// We allow for both "--net" and "--network", although the latter is the recommended way. // We allow for both "--net" and "--network", although the latter is the recommended way.
cmd.Flags().Var(&devOptions.NetMode, "net", "Connect a container to a network") cmd.Flags().Var(&devOptions.NetMode, "net", "Connect a container to a network, eg: [default|bridge|host|none|container:$CONTAINER_ID]")
cmd.Flags().Var(&devOptions.NetMode, "network", "Connect a container to a network") cmd.Flags().Var(&devOptions.NetMode, "network", "Connect a container to a network")
cmd.Flags().MarkHidden("net") cmd.Flags().MarkHidden("net")
// We allow for both "--net-alias" and "--network-alias", although the latter is the recommended way. // We allow for both "--net-alias" and "--network-alias", although the latter is the recommended way.
cmd.Flags().Var(&devOptions.Aliases, "net-alias", "Add network-scoped alias for the container") //cmd.Flags().Var(&devOptions.Aliases, "net-alias", "Add network-scoped alias for the container")
cmd.Flags().Var(&devOptions.Aliases, "network-alias", "Add network-scoped alias for the container") //cmd.Flags().Var(&devOptions.Aliases, "network-alias", "Add network-scoped alias for the container")
cmd.Flags().MarkHidden("net-alias") //cmd.Flags().MarkHidden("net-alias")
cmd.Flags().VarP(&devOptions.Volumes, "volume", "v", "Bind mount a volume") cmd.Flags().VarP(&devOptions.Volumes, "volume", "v", "Bind mount a volume")
cmd.Flags().Var(&devOptions.Mounts, "mount", "Attach a filesystem mount to the container") cmd.Flags().Var(&devOptions.Mounts, "mount", "Attach a filesystem mount to the container")
cmd.Flags().Var(&devOptions.Expose, "expose", "Expose a port or a range of ports") cmd.Flags().Var(&devOptions.Expose, "expose", "Expose a port or a range of ports")

View File

@@ -3,6 +3,9 @@ package dev
import ( import (
"context" "context"
"fmt" "fmt"
"math/rand"
"os"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"unsafe" "unsafe"
@@ -14,12 +17,18 @@ import (
"github.com/docker/go-connections/nat" "github.com/docker/go-connections/nat"
"github.com/google/uuid" "github.com/google/uuid"
miekgdns "github.com/miekg/dns" miekgdns "github.com/miekg/dns"
"github.com/moby/term"
v12 "github.com/opencontainers/image-spec/specs-go/v1" v12 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/spf13/cobra"
"k8s.io/api/core/v1" "k8s.io/api/core/v1"
v13 "k8s.io/apimachinery/pkg/apis/meta/v1" v13 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/cmd/util"
"github.com/wencaiwulue/kubevpn/pkg/config"
"github.com/wencaiwulue/kubevpn/pkg/cp"
"github.com/wencaiwulue/kubevpn/pkg/dns" "github.com/wencaiwulue/kubevpn/pkg/dns"
"github.com/wencaiwulue/kubevpn/pkg/handler"
) )
type RunConfig struct { type RunConfig struct {
@@ -143,7 +152,7 @@ func ConvertKubeResourceToContainer(namespace string, temp v1.PodTemplateSpec, e
r.k8sContainerName = c.Name r.k8sContainerName = c.Name
r.config = config r.config = config
r.hostConfig = hostConfig r.hostConfig = hostConfig
r.networkingConfig = nil r.networkingConfig = &network.NetworkingConfig{EndpointsConfig: make(map[string]*network.EndpointSettings)}
r.platform = /*&v12.Platform{Architecture: "amd64", OS: "linux"}*/ nil r.platform = /*&v12.Platform{Architecture: "amd64", OS: "linux"}*/ nil
runConfigList = append(runConfigList, &r) runConfigList = append(runConfigList, &r)
@@ -176,3 +185,63 @@ func GetDNS(ctx context.Context, f util.Factory, ns, pod string) (*miekgdns.Clie
} }
return fromPod, nil return fromPod, nil
} }
// GetVolume key format: [container name]-[volume mount name]
func GetVolume(ctx context.Context, f util.Factory, ns, pod string) (map[string][]mount.Mount, error) {
clientSet, err := f.KubernetesClientSet()
if err != nil {
return nil, err
}
var get *v1.Pod
get, err = clientSet.CoreV1().Pods(ns).Get(ctx, pod, v13.GetOptions{})
if err != nil {
return nil, err
}
result := map[string][]mount.Mount{}
for _, c := range get.Spec.Containers {
// if container name is vpn or envoy-proxy, not need to download volume
if c.Name == config.ContainerSidecarVPN || c.Name == config.ContainerSidecarEnvoyProxy {
continue
}
var m []mount.Mount
for _, volumeMount := range c.VolumeMounts {
if volumeMount.MountPath == "/tmp" {
continue
}
join := filepath.Join(os.TempDir(), strconv.Itoa(rand.Int()))
err = os.MkdirAll(join, 0755)
if err != nil {
return nil, err
}
if volumeMount.SubPath != "" {
join = filepath.Join(join, volumeMount.SubPath)
}
handler.RollbackFuncList = append(handler.RollbackFuncList, func() {
_ = os.RemoveAll(join)
})
// pod-namespace/pod-name:path
remotePath := fmt.Sprintf("%s/%s:%s", ns, pod, volumeMount.MountPath)
stdIn, stdOut, stdErr := term.StdStreams()
copyOptions := cp.NewCopyOptions(genericclioptions.IOStreams{In: stdIn, Out: stdOut, ErrOut: stdErr})
copyOptions.Container = c.Name
copyOptions.MaxTries = 10
err = copyOptions.Complete(f, &cobra.Command{}, []string{remotePath, join})
if err != nil {
return nil, err
}
err = copyOptions.Run()
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to download volume %s path %s to %s, err: %v, ignore...\n", volumeMount.Name, remotePath, join, err)
continue
}
m = append(m, mount.Mount{
Type: mount.TypeBind,
Source: join,
Target: volumeMount.MountPath,
})
fmt.Printf("%s:%s\n", join, volumeMount.MountPath)
}
result[c.Name] = m
}
return result, nil
}

View File

@@ -51,7 +51,7 @@ type Options struct {
Expose opts.ListOpts Expose opts.ListOpts
ExtraHosts opts.ListOpts ExtraHosts opts.ListOpts
NetMode opts.NetworkOpt NetMode opts.NetworkOpt
Aliases opts.ListOpts //Aliases opts.ListOpts
Env opts.ListOpts Env opts.ListOpts
Mounts opts.MountOpt Mounts opts.MountOpt
Volumes opts.ListOpts Volumes opts.ListOpts
@@ -99,7 +99,7 @@ func (d Options) Main(ctx context.Context) error {
if err != nil { if err != nil {
return err return err
} }
volume, err := util.GetVolume(ctx, d.Factory, d.Namespace, pod) volume, err := GetVolume(ctx, d.Factory, d.Namespace, pod)
if err != nil { if err != nil {
return err return err
} }
@@ -232,7 +232,7 @@ func (r Run) Run(ctx context.Context, volume map[string][]mount.Mount) error {
id, err = run(ctx, config, cli) id, err = run(ctx, config, cli)
if err != nil { if err != nil {
// try another way to startup container // try another way to startup container
log.Info("try another way to startup container") log.Infof("occur err: %v, try another way to startup container...", err)
config.hostConfig.Mounts = nil config.hostConfig.Mounts = nil
id, err = run(ctx, config, cli) id, err = run(ctx, config, cli)
if err != nil { if err != nil {

View File

@@ -148,11 +148,6 @@ func fillOptions(r Run, copts Options) error {
config.hostConfig.Binds = binds config.hostConfig.Binds = binds
// todo
if copts.Aliases.Len() != 0 {
//config.networkingConfig.EndpointsConfig
}
return nil return nil
} }

View File

@@ -70,6 +70,11 @@ func run(ctx context.Context, runConfig *RunConfig, cli *client.Client) (id stri
} }
id = create.ID id = create.ID
log.Infof("Created container: %s", name) log.Infof("Created container: %s", name)
defer func() {
if err != nil {
_ = cli.ContainerRemove(ctx, id, types.ContainerRemoveOptions{Force: true})
}
}()
err = cli.ContainerStart(ctx, create.ID, types.ContainerStartOptions{}) err = cli.ContainerStart(ctx, create.ID, types.ContainerStartOptions{})
if err != nil { if err != nil {

View File

@@ -53,7 +53,7 @@ func SetupDNS(clientConfig *miekgdns.ClientConfig, _ []string) error {
func CancelDNS() { func CancelDNS() {
updateHosts("") updateHosts("")
getenv := os.Getenv("luid") getenv := os.Getenv(config.EnvTunNameOrLUID)
parseUint, err := strconv.ParseUint(getenv, 10, 64) parseUint, err := strconv.ParseUint(getenv, 10, 64)
if err != nil { if err != nil {
log.Warningln(err) log.Warningln(err)
@@ -61,6 +61,7 @@ func CancelDNS() {
} }
luid := winipcfg.LUID(parseUint) luid := winipcfg.LUID(parseUint)
_ = luid.FlushDNS(windows.AF_INET) _ = luid.FlushDNS(windows.AF_INET)
_ = luid.FlushRoutes(windows.AF_INET)
} }
func updateNicMetric(name string) error { func updateNicMetric(name string) error {

View File

@@ -33,11 +33,11 @@ func InstallWireGuardTunDriver() {
} }
func UninstallWireGuardTunDriver() error { func UninstallWireGuardTunDriver() error {
wd, err := os.Getwd() executable, err := os.Executable()
if err != nil { if err != nil {
return err return err
} }
filename := filepath.Join(wd, "wintun.dll") filename := filepath.Join(filepath.Dir(executable), "wintun.dll")
return os.Remove(filename) return os.Remove(filename)
} }

View File

@@ -27,34 +27,34 @@ var stopChan = make(chan os.Signal)
var RollbackFuncList = make([]func(), 2) var RollbackFuncList = make([]func(), 2)
var ctx, cancel = context.WithCancel(context.Background()) var ctx, cancel = context.WithCancel(context.Background())
func (c *ConnectOptions) addCleanUpResourceHandler(clientset *kubernetes.Clientset, namespace string) { func (c *ConnectOptions) addCleanUpResourceHandler() {
signal.Notify(stopChan, os.Interrupt, os.Kill, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGKILL /*, syscall.SIGSTOP*/) signal.Notify(stopChan, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGKILL)
go func() { go func() {
<-stopChan <-stopChan
log.Info("prepare to exit, cleaning up") log.Info("prepare to exit, cleaning up")
dns.CancelDNS()
err := c.dhcp.ReleaseIpToDHCP(c.usedIPs...) err := c.dhcp.ReleaseIpToDHCP(c.usedIPs...)
if err != nil { if err != nil {
log.Errorf("failed to release ip to dhcp, err: %v", err) log.Errorf("failed to release ip to dhcp, err: %v", err)
} }
cancel()
for _, function := range RollbackFuncList { for _, function := range RollbackFuncList {
if function != nil { if function != nil {
function() function()
} }
} }
_ = clientset.CoreV1().Pods(namespace).Delete(context.Background(), config.CniNetName, v1.DeleteOptions{GracePeriodSeconds: pointer.Int64(0)}) _ = c.clientset.CoreV1().Pods(c.Namespace).Delete(context.Background(), config.CniNetName, v1.DeleteOptions{GracePeriodSeconds: pointer.Int64(0)})
var count int var count int
count, err = updateRefCount(clientset.CoreV1().ConfigMaps(namespace), config.ConfigMapPodTrafficManager, -1) count, err = updateRefCount(c.clientset.CoreV1().ConfigMaps(c.Namespace), config.ConfigMapPodTrafficManager, -1)
if err == nil { if err == nil {
// if ref-count is less than zero or equals to zero, means nobody is using this traffic pod, so clean it // if ref-count is less than zero or equals to zero, means nobody is using this traffic pod, so clean it
if count <= 0 { if count <= 0 {
log.Info("ref-count is zero, prepare to clean up resource") log.Info("ref-count is zero, prepare to clean up resource")
cleanup(clientset, namespace, config.ConfigMapPodTrafficManager, true) cleanup(c.clientset, c.Namespace, config.ConfigMapPodTrafficManager, true)
} }
} else { } else {
log.Error(err) log.Error(err)
} }
dns.CancelDNS()
cancel()
log.Info("clean up successful") log.Info("clean up successful")
util.CleanExtensionLib() util.CleanExtensionLib()
os.Exit(0) os.Exit(0)

View File

@@ -71,7 +71,6 @@ func (c *ConnectOptions) createRemoteInboundPod(ctx1 context.Context) (err error
} }
for _, workload := range c.Workloads { for _, workload := range c.Workloads {
if len(workload) > 0 {
configInfo := util.PodRouteConfig{ configInfo := util.PodRouteConfig{
LocalTunIP: c.localTunIP.IP.String(), LocalTunIP: c.localTunIP.IP.String(),
TrafficManagerRealIP: c.routerIP.String(), TrafficManagerRealIP: c.routerIP.String(),
@@ -86,7 +85,6 @@ func (c *ConnectOptions) createRemoteInboundPod(ctx1 context.Context) (err error
return err return err
} }
} }
}
return return
} }
@@ -115,7 +113,7 @@ func Rollback(f cmdutil.Factory, ns, workload string) {
} }
func (c *ConnectOptions) DoConnect() (err error) { func (c *ConnectOptions) DoConnect() (err error) {
c.addCleanUpResourceHandler(c.clientset, c.Namespace) c.addCleanUpResourceHandler()
trafficMangerNet := net.IPNet{IP: config.RouterIP, Mask: config.CIDR.Mask} trafficMangerNet := net.IPNet{IP: config.RouterIP, Mask: config.CIDR.Mask}
c.dhcp = NewDHCPManager(c.clientset.CoreV1().ConfigMaps(c.Namespace), c.Namespace, &trafficMangerNet) c.dhcp = NewDHCPManager(c.clientset.CoreV1().ConfigMaps(c.Namespace), c.Namespace, &trafficMangerNet)
if err = c.dhcp.InitDHCP(ctx); err != nil { if err = c.dhcp.InitDHCP(ctx); err != nil {

View File

@@ -5,24 +5,13 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"math/rand"
"os"
"path/filepath"
"strconv"
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"github.com/docker/docker/api/types/mount"
"github.com/moby/term"
"github.com/spf13/cobra"
"golang.org/x/exp/constraints" "golang.org/x/exp/constraints"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/cmd/util"
"github.com/wencaiwulue/kubevpn/pkg/config"
"github.com/wencaiwulue/kubevpn/pkg/cp"
) )
func PrintStatus(pod *corev1.Pod, writer io.Writer) { func PrintStatus(pod *corev1.Pod, writer io.Writer) {
@@ -103,60 +92,3 @@ func GetEnv(ctx context.Context, f util.Factory, ns, pod string) (map[string][]s
} }
return result, nil return result, nil
} }
// GetVolume key format: [container name]-[volume mount name]
func GetVolume(ctx context.Context, f util.Factory, ns, pod string) (map[string][]mount.Mount, error) {
clientSet, err := f.KubernetesClientSet()
if err != nil {
return nil, err
}
var get *corev1.Pod
get, err = clientSet.CoreV1().Pods(ns).Get(ctx, pod, v1.GetOptions{})
if err != nil {
return nil, err
}
result := map[string][]mount.Mount{}
for _, c := range get.Spec.Containers {
// if container name is vpn or envoy-proxy, not need to download volume
if c.Name == config.ContainerSidecarVPN || c.Name == config.ContainerSidecarEnvoyProxy {
continue
}
var m []mount.Mount
for _, volumeMount := range c.VolumeMounts {
if volumeMount.MountPath == "/tmp" {
continue
}
join := filepath.Join(os.TempDir(), strconv.Itoa(rand.Int()))
err = os.MkdirAll(join, 0755)
if err != nil {
return nil, err
}
if volumeMount.SubPath != "" {
join = filepath.Join(join, volumeMount.SubPath)
}
// pod-namespace/pod-name:path
remotePath := fmt.Sprintf("%s/%s:%s", ns, pod, volumeMount.MountPath)
stdIn, stdOut, stdErr := term.StdStreams()
copyOptions := cp.NewCopyOptions(genericclioptions.IOStreams{In: stdIn, Out: stdOut, ErrOut: stdErr})
copyOptions.Container = c.Name
copyOptions.MaxTries = 10
err = copyOptions.Complete(f, &cobra.Command{}, []string{remotePath, join})
if err != nil {
return nil, err
}
err = copyOptions.Run()
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "failed to download volume %s path %s to %s, err: %v, ignore...\n", volumeMount.Name, remotePath, join, err)
continue
}
m = append(m, mount.Mount{
Type: mount.TypeBind,
Source: join,
Target: volumeMount.MountPath,
})
fmt.Printf("%s:%s\n", join, volumeMount.MountPath)
}
result[c.Name] = m
}
return result, nil
}

View File

@@ -31,6 +31,7 @@ import (
k8sruntime "k8s.io/apimachinery/pkg/runtime" k8sruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch" "k8s.io/apimachinery/pkg/watch"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
runtimeresource "k8s.io/cli-runtime/pkg/resource" runtimeresource "k8s.io/cli-runtime/pkg/resource"
@@ -631,19 +632,36 @@ func AllContainerIsRunning(pod *v1.Pod) bool {
} }
func CleanExtensionLib() { func CleanExtensionLib() {
if IsWindows() { if !IsWindows() {
err := retry.OnError(retry.DefaultRetry, func(err error) bool { return
return err != nil }
}, func() error { path, err := os.Executable()
return driver.UninstallWireGuardTunDriver()
})
if err != nil {
var wd string
wd, err = os.Getwd()
if err != nil { if err != nil {
return return
} }
filename := filepath.Join(wd, "wintun.dll") filename := filepath.Join(filepath.Dir(path), "wintun.dll")
_ = retry.OnError(
// step : 0 13 34 55 100 194 433 661 1384 2689 (ms)
// total: 5.57s
wait.Backoff{
Steps: 10,
Duration: 10 * time.Millisecond,
Factor: 2.0,
Jitter: 0.5,
},
func(error) bool {
_, err = os.Lstat(filename)
return !errors.Is(err, os.ErrNotExist)
},
func() error {
err = driver.UninstallWireGuardTunDriver()
return fmt.Errorf("%v", err)
},
)
_, err = os.Lstat(filename)
if errors.Is(err, os.ErrNotExist) {
return
}
var temp *os.File var temp *os.File
if temp, err = os.CreateTemp("", ""); err != nil { if temp, err = os.CreateTemp("", ""); err != nil {
return return
@@ -651,11 +669,12 @@ func CleanExtensionLib() {
if err = temp.Close(); err != nil { if err = temp.Close(); err != nil {
return return
} }
if err = os.Remove(temp.Name()); err != nil {
return
}
if err = os.Rename(filename, temp.Name()); err != nil { if err = os.Rename(filename, temp.Name()); err != nil {
log.Debugln(err) log.Debugln(err)
} }
}
}
} }
func WaitPodToBeReady(ctx context.Context, podInterface v12.PodInterface, selector metav1.LabelSelector) error { func WaitPodToBeReady(ctx context.Context, podInterface v12.PodInterface, selector metav1.LabelSelector) error {

View File

@@ -3,6 +3,7 @@ package util
import ( import (
"encoding/json" "encoding/json"
"net" "net"
"strings"
"testing" "testing"
"github.com/containernetworking/cni/libcni" "github.com/containernetworking/cni/libcni"
@@ -94,6 +95,9 @@ func TestPing(t *testing.T) {
} }
ipConn, err := net.ListenPacket("ip4:icmp", "0.0.0.0") ipConn, err := net.ListenPacket("ip4:icmp", "0.0.0.0")
if err != nil { if err != nil {
if strings.Contains(err.Error(), "operation not permitted") {
return
}
t.Error(err) t.Error(err)
} }
bytes := buf.Bytes() bytes := buf.Bytes()