From cbd6cfd5a554aa4b28c280457c53dad8be44e1ac Mon Sep 17 00:00:00 2001 From: liu-hm19 Date: Mon, 22 Apr 2024 17:48:18 +0800 Subject: [PATCH] feat: apply supports port forwarding (#1057) --- go.mod | 31 ++-- go.sum | 64 +++---- pkg/cmd/apply/apply.go | 75 +++++++- pkg/engine/operation/port_forward.go | 217 ++++++++++++++++++++++ pkg/engine/operation/port_forward_test.go | 101 ++++++++++ 5 files changed, 430 insertions(+), 58 deletions(-) create mode 100644 pkg/engine/operation/port_forward.go create mode 100644 pkg/engine/operation/port_forward_test.go diff --git a/go.mod b/go.mod index 85f36cee..166ad757 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module kusionstack.io/kusion -go 1.22 +go 1.22.1 require ( github.com/AlecAivazis/survey/v2 v2.3.4 @@ -50,31 +50,31 @@ require ( github.com/jinzhu/copier v0.3.2 github.com/lucasb-eyer/go-colorful v1.0.3 github.com/mitchellh/hashstructure v1.0.0 - github.com/onsi/ginkgo/v2 v2.13.0 - github.com/onsi/gomega v1.30.0 + github.com/onsi/ginkgo/v2 v2.15.0 + github.com/onsi/gomega v1.31.0 github.com/pkg/errors v0.9.1 github.com/pterm/pterm v0.12.60 github.com/pulumi/pulumi/sdk/v3 v3.68.0 github.com/sergi/go-diff v1.3.1 github.com/spf13/afero v1.6.0 - github.com/spf13/cobra v1.7.0 + github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.9.0 github.com/swaggo/http-swagger v1.3.4 github.com/swaggo/swag v1.16.3 github.com/texttheater/golang-levenshtein v1.0.1 github.com/tidwall/gjson v1.17.0 github.com/zclconf/go-cty v1.12.1 - go.uber.org/zap v1.24.0 + go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 - gopkg.in/natefinch/lumberjack.v2 v2.0.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/mysql v1.5.4 gorm.io/gorm v1.25.7 - k8s.io/api v0.29.2 - k8s.io/apimachinery v0.29.2 + k8s.io/api v0.30.0 + k8s.io/apimachinery v0.30.0 k8s.io/cli-runtime v0.29.2 - k8s.io/client-go v0.29.2 + k8s.io/client-go v0.30.0 k8s.io/component-base v0.27.2 k8s.io/kubectl v0.27.1 k8s.io/utils v0.0.0-20230726121419-3b25d923346b @@ -206,7 +206,7 @@ require ( github.com/cloudflare/circl v1.3.7 // indirect github.com/containerd/console v1.0.3 // indirect github.com/containerd/containerd v1.7.6 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/docker/cli v24.0.6+incompatible // indirect github.com/docker/distribution v2.8.2+incompatible // indirect @@ -221,7 +221,7 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect - github.com/go-logr/logr v1.3.0 + github.com/go-logr/logr v1.4.1 github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect @@ -312,12 +312,11 @@ require ( go.opentelemetry.io/otel v1.21.0 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.6.0 // indirect + go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.1.0 // indirect golang.org/x/crypto v0.21.0 golang.org/x/mod v0.16.0 // indirect - golang.org/x/net v0.22.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.18.0 // indirect @@ -332,8 +331,8 @@ require ( google.golang.org/protobuf v1.33.0 gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - k8s.io/klog/v2 v2.110.1 - k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/klog/v2 v2.120.1 + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect oras.land/oras-go/v2 v2.3.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect diff --git a/go.sum b/go.sum index c739c60d..80bad384 100644 --- a/go.sum +++ b/go.sum @@ -349,8 +349,6 @@ github.com/aws/smithy-go v1.17.0 h1:wWJD7LX6PBV6etBUwO0zElG0nWN9rUhp0WdYeHSHAaI= github.com/aws/smithy-go v1.17.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f h1:ZNv7On9kyUzm7fvRZumSyy/IUiSC7AzL0I1jKKtwooA= github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f/go.mod h1:AuiFmCCPBSrqvVMvuqFuk0qogytodnVFVSN5CeJB8Gc= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -421,8 +419,9 @@ github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -525,8 +524,8 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= @@ -931,16 +930,16 @@ github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0 github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= -github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= -github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= +github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= @@ -1031,8 +1030,8 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1134,15 +1133,12 @@ go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqe go.starlark.net v0.0.0-20200707032745-474f21a9602d/go.mod h1:f0znQkUKRrkk36XxWbGjMqQM8wGv/xHBVE2qc3B5oFU= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= -go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.1.0 h1:oMxhUYsO9VsR1dcoVUjJjIGhx1LXol3989T/yZ59Xsw= golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1268,8 +1264,8 @@ golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1749,8 +1745,8 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.66.2 h1:XfR1dOYubytKy4Shzc2LHrrGhU0lDCfDGG1yLPmpgsI= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= -gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= @@ -1783,22 +1779,22 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= -k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0= +k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= +k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= k8s.io/apiextensions-apiserver v0.27.2 h1:iwhyoeS4xj9Y7v8YExhUwbVuBhMr3Q4bd/laClBV6Bo= k8s.io/apiextensions-apiserver v0.27.2/go.mod h1:Oz9UdvGguL3ULgRdY9QMUzL2RZImotgxvGjdWRq6ZXQ= -k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= -k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= +k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/cli-runtime v0.29.2 h1:smfsOcT4QujeghsNjECKN3lwyX9AwcFU0nvJ7sFN3ro= k8s.io/cli-runtime v0.29.2/go.mod h1:KLisYYfoqeNfO+MkTWvpqIyb1wpJmmFJhioA0xd4MW8= -k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= -k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA= +k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= +k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= k8s.io/component-base v0.27.2 h1:neju+7s/r5O4x4/txeUONNTS9r1HsPbyoPBAtHsDCpo= k8s.io/component-base v0.27.2/go.mod h1:5UPk7EjfgrfgRIuDBFtsEFAe4DAvP3U+M8RTzoSJkpo= -k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= -k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= -k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= -k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/kubectl v0.27.1 h1:9T5c5KdpburYiW8XKQSH0Uly1kMNE90aGSnbYUZNdcA= k8s.io/kubectl v0.27.1/go.mod h1:QsAkSmrRsKTPlAFzF8kODGDl4p35BIwQnc9XFhkcsy8= k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= diff --git a/pkg/cmd/apply/apply.go b/pkg/cmd/apply/apply.go index 2215bcf6..863cfd06 100644 --- a/pkg/cmd/apply/apply.go +++ b/pkg/cmd/apply/apply.go @@ -50,7 +50,7 @@ var ( applyExample = i18n.T(` # Apply with specified work directory kusion apply -w /path/to/workdir - + # Apply with specified arguments kusion apply -D name=test -D age=18 @@ -58,7 +58,10 @@ var ( kusion apply --yes # Apply without output style and color - kusion apply --no-style=true`) + kusion apply --no-style=true + + # Apply with localhost port forwarding + kusion apply --port-forward=8080`) ) // ApplyFlags directly reflect the information that CLI is gathering via flags. They will be converted to @@ -68,9 +71,10 @@ var ( type ApplyFlags struct { *preview.PreviewFlags - Yes bool - DryRun bool - Watch bool + Yes bool + DryRun bool + Watch bool + PortForward int genericiooptions.IOStreams } @@ -79,9 +83,10 @@ type ApplyFlags struct { type ApplyOptions struct { *preview.PreviewOptions - Yes bool - DryRun bool - Watch bool + Yes bool + DryRun bool + Watch bool + PortForward int genericiooptions.IOStreams } @@ -126,6 +131,7 @@ func (f *ApplyFlags) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVarP(&f.Yes, "yes", "y", false, i18n.T("Automatically approve and perform the update after previewing it")) cmd.Flags().BoolVarP(&f.DryRun, "dry-run", "", false, i18n.T("Preview the execution effect (always successful) without actually applying the changes")) cmd.Flags().BoolVarP(&f.Watch, "watch", "", false, i18n.T("After creating/updating/deleting the requested object, watch for changes")) + cmd.Flags().IntVarP(&f.PortForward, "port-forward", "", 0, i18n.T("Forward an available local port to the specified service port")) } // ToOptions converts from CLI inputs to runtime inputs. @@ -141,6 +147,7 @@ func (f *ApplyFlags) ToOptions() (*ApplyOptions, error) { Yes: f.Yes, DryRun: f.DryRun, Watch: f.Watch, + PortForward: f.PortForward, IOStreams: f.IOStreams, } @@ -153,6 +160,10 @@ func (o *ApplyOptions) Validate(cmd *cobra.Command, args []string) error { return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args) } + if o.PortForward < 0 || o.PortForward > 65535 { + return cmdutil.UsageErrorf(cmd, "Invalid port number to forward: %d, must be between 1 and 65535", o.PortForward) + } + return nil } @@ -239,6 +250,13 @@ func (o *ApplyOptions) Run() error { } } + if o.PortForward > 0 { + fmt.Printf("\nStart port-forwarding ...\n") + if err = PortForward(o, spec); err != nil { + return err + } + } + return nil } @@ -433,6 +451,47 @@ func Watch( return nil } +// PortForward function will forward an available local port to the specified port +// of the project Kubernetes Service. +// +// Example: +// +// o := NewApplyOptions() +// spec, err := generate.GenerateSpecWithSpinner(o.RefProject, o.RefStack, o.RefWorkspace, nil, o.NoStyle) +// +// if err != nil { +// return err +// } +// +// err = PortForward(o, spec) +// +// if err != nil { +// return err +// } +func PortForward( + o *ApplyOptions, + spec *apiv1.Spec, +) error { + if o.DryRun { + fmt.Println("NOTE: Portforward doesn't work in DryRun mode") + return nil + } + + // portforward operation + wo := &operation.PortForwardOperation{} + if err := wo.PortForward(&operation.PortForwardRequest{ + Request: models.Request{ + Intent: spec, + }, + Port: o.PortForward, + }); err != nil { + return err + } + + fmt.Println("Portforward has been completed!") + return nil +} + type lineSummary struct { created, updated, deleted int } diff --git a/pkg/engine/operation/port_forward.go b/pkg/engine/operation/port_forward.go new file mode 100644 index 00000000..35ac2fa8 --- /dev/null +++ b/pkg/engine/operation/port_forward.go @@ -0,0 +1,217 @@ +package operation + +import ( + "context" + "errors" + "fmt" + "math/rand" + "net" + "net/http" + "os" + "strings" + + yamlv2 "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + k8syaml "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" + "kusionstack.io/kusion/pkg/engine/operation/models" + "kusionstack.io/kusion/pkg/engine/printers/convertor" + "kusionstack.io/kusion/pkg/engine/runtime/kubernetes/kubeops" +) + +var ( + ErrEmptySpec = errors.New("empty resources in spec") + ErrEmptyService = errors.New("empty k8s service") + ErrNotOneSvcWithTargetPort = errors.New("only support one k8s service to forward with target port") +) + +type PortForwardOperation struct { + models.Operation +} + +type PortForwardRequest struct { + models.Request `json:",inline" yaml:",inline"` + Port int +} + +func (bpo *PortForwardOperation) PortForward(req *PortForwardRequest) error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if req.Intent == nil { + return ErrEmptySpec + } + + // Find Kubernetes Service in the resources of Spec. + services := make(map[*v1.Resource]*corev1.Service) + for _, res := range req.Intent.Resources { + // Skip non-Kubernetes resources. + if res.Type != v1.Kubernetes { + continue + } + + // Convert interface{} to unstructured. + rYaml, err := yamlv2.Marshal(res.Attributes) + if err != nil { + return fmt.Errorf("failed to convert resource attributes to unstructured raw yaml: %v", err) + } + + // Decode YAML manifest into unstructured.Unstructured. + decUnstructured := k8syaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme) + obj := &unstructured.Unstructured{} + + _, _, err = decUnstructured.Decode(rYaml, nil, obj) + if err != nil { + return fmt.Errorf("failed to decode yaml manifest into unstructured object: %v", err) + } + + if obj.GetKind() != convertor.Service { + continue + } + + convertedObj := convertor.ToK8s(obj) + services[&res] = convertedObj.(*corev1.Service) + } + + if len(services) == 0 { + return ErrEmptyService + } + + filteredServices := make(map[*v1.Resource]*corev1.Service) + for res, svc := range services { + targetPortFound := false + for _, port := range svc.Spec.Ports { + if port.Port == int32(req.Port) { + targetPortFound = true + continue + } + } + + if targetPortFound { + filteredServices[res] = svc + } + } + services = filteredServices + + if len(services) != 1 { + return ErrNotOneSvcWithTargetPort + } + + // Port-forward the Service with client-go. + var localPort int + failed := make(chan error) + for res, svc := range services { + namespace := svc.GetNamespace() + serviceName := svc.GetName() + + var servicePort int + if req.Port == 0 { + // We will use the first port in Service if not specified. + servicePort = int(svc.Spec.Ports[0].Port) + } else { + servicePort = req.Port + } + + cfg, err := clientcmd.BuildConfigFromFlags("", kubeops.GetKubeConfig(res)) + if err != nil { + return err + } + + clientset, err := kubernetes.NewForConfig(cfg) + if err != nil { + return err + } + + // Get an available local port for forwarding. + minPort, maxPort := 1024, 65535 + + for { + localPort = rand.Intn(maxPort-minPort) + minPort + + // Try to listen to the port. + addr := fmt.Sprintf(":%d", localPort) + listener, err := net.Listen("tcp", addr) + if err != nil { + continue + } + _ = listener.Close() + + break + } + + go func() { + err := ForwardPort(ctx, cfg, clientset, namespace, serviceName, servicePort, localPort) + failed <- err + }() + } + + err := <-failed + return err +} + +func ForwardPort( + ctx context.Context, + restConfig *rest.Config, + clientset *kubernetes.Clientset, + namespace, serviceName string, + servicePort, localPort int, +) error { + svc, err := clientset.CoreV1().Services(namespace).Get(ctx, serviceName, metav1.GetOptions{}) + if err != nil { + return err + } + if svc == nil { + return fmt.Errorf("failed to find service: %s", serviceName) + } + + labels := []string{} + for k, v := range svc.Spec.Selector { + labels = append(labels, strings.Join([]string{k, v}, "=")) + } + label := strings.Join(labels, ",") + + // Select the first pod to forward the target port. + pods, err := clientset.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ + LabelSelector: label, Limit: 1, + }) + if err != nil { + return err + } + if len(pods.Items) < 1 { + return fmt.Errorf("pods of the service '%s' not found", serviceName) + } + pod := pods.Items[0] + + fmt.Printf("Forwarding localhost port to targetPort of pod '%s' selected by the service '%s' (%d:%d)\n", + pod.Name, serviceName, localPort, servicePort) + + // Build a URL for SPDY connection for port-forwarding. + url := clientset.CoreV1().RESTClient().Post(). + Resource("pods").Namespace(pod.Namespace).Name(pod.Name). + SubResource("portforward").URL() + + transport, upgrader, err := spdy.RoundTripperFor(restConfig) + if err != nil { + return err + } + + ports := []string{fmt.Sprintf("%d:%d", localPort, servicePort)} + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, "POST", url) + + stop, ready := make(chan struct{}, 1), make(chan struct{}) + out, errOut := os.Stdout, os.Stderr + + fw, err := portforward.NewOnAddresses(dialer, []string{"localhost"}, ports, stop, ready, out, errOut) + if err != nil { + return err + } + + return fw.ForwardPorts() +} diff --git a/pkg/engine/operation/port_forward_test.go b/pkg/engine/operation/port_forward_test.go new file mode 100644 index 00000000..6bdb47a7 --- /dev/null +++ b/pkg/engine/operation/port_forward_test.go @@ -0,0 +1,101 @@ +package operation + +import ( + "testing" + + "github.com/stretchr/testify/assert" + v1 "kusionstack.io/kusion/pkg/apis/api.kusion.io/v1" + "kusionstack.io/kusion/pkg/engine/operation/models" +) + +func TestPortForwardOperation_PortForward(t *testing.T) { + testcases := []struct { + name string + req *PortForwardRequest + expectedErr error + }{ + { + name: "empty spec", + req: &PortForwardRequest{ + Port: 8080, + }, + expectedErr: ErrEmptySpec, + }, + { + name: "empty services", + req: &PortForwardRequest{ + Request: models.Request{ + Intent: &v1.Spec{ + Resources: v1.Resources{ + { + ID: "v1:Namespace:quickstart", + Type: "Kubernetes", + Attributes: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": map[string]interface{}{ + "name": "quickstart", + }, + }, + }, + }, + }, + }, + Port: 8080, + }, + expectedErr: ErrEmptyService, + }, + { + name: "not one service with target port", + req: &PortForwardRequest{ + Request: models.Request{ + Intent: &v1.Spec{ + Resources: v1.Resources{ + { + ID: "v1:Service:quickstart:quickstart-dev-quickstart-private", + Type: "Kubernetes", + Attributes: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Service", + "metadata": map[string]interface{}{ + "name": "quickstart-dev-quickstart-private", + "namespace": "quickstart", + }, + "spec": map[string]interface{}{ + "ports": []interface{}{ + map[string]interface{}{ + "name": "quickstart-dev-quickstart-private-8080-tcp", + "port": 8888, + "protocol": "TCP", + "targetPort": 8888, + }, + }, + "selector": map[string]interface{}{ + "app.kubernetes.io/name": "quickstart", + }, + "type": "ClusterIP", + }, + }, + }, + }, + }, + }, + Port: 8080, + }, + expectedErr: ErrNotOneSvcWithTargetPort, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + bpo := &PortForwardOperation{} + err := bpo.PortForward(tc.req) + + if tc.expectedErr != nil { + assert.ErrorContains(t, err, tc.expectedErr.Error()) + } else { + assert.NoError(t, err) + } + }) + } +}