diff --git a/.github/workflows/build-and-test.yaml b/.github/workflows/build-and-test.yaml index c8d67dc13..b519c2403 100644 --- a/.github/workflows/build-and-test.yaml +++ b/.github/workflows/build-and-test.yaml @@ -2,18 +2,20 @@ name: "Build and Test" on: push: - branches: [ main ] + branches: [main] pull_request: branches: ["*"] +env: + GO_VERSION: 1.24 jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version: 1.22 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} # There are too many lint errors in current code bases # uncomment when we decide what lint should be addressed or ignored. # - run: make lint @@ -21,40 +23,40 @@ jobs: coverage-test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: "Setup Go" - uses: actions/setup-go@v5 - with: - go-version: 1.22 + - name: "Setup Go" + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} - - name: Setup Golang Caches - uses: actions/cache@v4 - with: - path: |- - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ github.run_id }} - restore-keys: ${{ runner.os }}-go + - name: Setup Golang Caches + uses: actions/cache@v4 + with: + path: |- + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ github.run_id }} + restore-keys: ${{ runner.os }}-go - - run: git stash # restore patch + - run: git stash # restore patch - # test - - name: Run Coverage Tests - run: |- - go version - GOPROXY="https://proxy.golang.org,direct" make go.test.coverage - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - fail_ci_if_error: false - files: ./coverage.xml - verbose: true + # test + - name: Run Coverage Tests + run: |- + go version + GOPROXY="https://proxy.golang.org,direct" make go.test.coverage + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + fail_ci_if_error: false + files: ./coverage.xml + verbose: true build: # The type of runner that the job will run on runs-on: ubuntu-latest - needs: [lint,coverage-test] + needs: [lint, coverage-test] steps: - name: "Checkout ${{ github.ref }}" uses: actions/checkout@v4 @@ -64,7 +66,7 @@ jobs: - name: "Setup Go" uses: actions/setup-go@v5 with: - go-version: 1.22 + go-version: ${{ env.GO_VERSION }} - name: Setup Golang Caches uses: actions/cache@v4 @@ -90,45 +92,52 @@ jobs: runs-on: ubuntu-latest needs: [build] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v3 higress-conformance-test: runs-on: ubuntu-latest needs: [build] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧 - uses: jlumbroso/free-disk-space@main - with: - tool-cache: false - android: true - dotnet: true - haskell: true - large-packages: true - swap-storage: true - - - name: "Setup Go" - uses: actions/setup-go@v5 - with: - go-version: 1.22 + - name: Free Up GitHub Actions Ubuntu Runner Disk Space 🔧 + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: true + swap-storage: true - - name: Setup Golang Caches - uses: actions/cache@v4 - with: - path: |- - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ github.run_id }} - restore-keys: ${{ runner.os }}-go - - - run: git stash # restore patch + - name: "Setup Go" + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Setup Golang Caches + uses: actions/cache@v4 + with: + path: |- + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ github.run_id }} + # key: ${{ runner.os }}-go-${{ env.GO_VERSION }} + + restore-keys: ${{ runner.os }}-go + + - run: git stash # restore patch + + - name: update go mod + run: |- + make prebuild + go mod tidy + + - name: "Run Higress E2E Conformance Tests" + run: GOPROXY="https://proxy.golang.org,direct" make higress-conformance-test - - name: "Run Higress E2E Conformance Tests" - run: GOPROXY="https://proxy.golang.org,direct" make higress-conformance-test - publish: runs-on: ubuntu-latest - needs: [higress-conformance-test,gateway-conformance-test] + needs: [higress-conformance-test, gateway-conformance-test] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v4 diff --git a/hgctl/go.mod b/hgctl/go.mod index 62e91f3e9..96fef7a07 100644 --- a/hgctl/go.mod +++ b/hgctl/go.mod @@ -1,8 +1,8 @@ module github.com/alibaba/higress/hgctl -go 1.22.2 +go 1.23.0 -toolchain go1.23.7 +toolchain go1.24.1 replace github.com/spf13/viper => github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c @@ -20,6 +20,7 @@ replace github.com/alibaba/higress/v2 => ../ require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/alibaba/higress/v2 v2.0.0-00010101000000-000000000000 + github.com/braydonk/yaml v0.7.0 github.com/compose-spec/compose-go v1.17.0 github.com/docker/cli v24.0.7+incompatible github.com/docker/compose/v2 v2.23.3 @@ -28,6 +29,7 @@ require ( github.com/fatih/color v1.15.0 github.com/fatih/structtag v1.2.0 github.com/google/yamlfmt v0.10.0 + github.com/higress-group/openapi-to-mcpserver v0.0.0-20250925065334-de60a170f950 github.com/iancoleman/orderedmap v0.3.0 github.com/kylelemons/godebug v1.1.0 github.com/mitchellh/go-homedir v1.1.0 @@ -37,7 +39,7 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.10.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 helm.sh/helm/v3 v3.12.2 @@ -51,6 +53,8 @@ require ( sigs.k8s.io/yaml v1.4.0 ) +require github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect + require ( github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect @@ -79,7 +83,6 @@ require ( github.com/aws/smithy-go v1.13.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect - github.com/braydonk/yaml v0.7.0 // indirect github.com/buger/goterm v1.0.4 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect @@ -111,13 +114,14 @@ require ( github.com/fsnotify/fsevents v0.1.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fvbommel/sortorder v1.1.0 // indirect + github.com/getkin/kin-openapi v0.118.0 // indirect github.com/go-errors/errors v1.4.2 // indirect github.com/go-gorp/gorp/v3 v3.0.5 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect - github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/gofrs/flock v0.8.1 // indirect @@ -147,6 +151,7 @@ require ( github.com/imdario/mergo v1.0.0 // indirect github.com/in-toto/in-toto-golang v0.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/yaml v0.1.0 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect github.com/jonboulle/clockwork v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -165,6 +170,7 @@ require ( github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/manifoldco/promptui v0.9.0 github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect @@ -187,6 +193,7 @@ require ( github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -194,6 +201,7 @@ require ( github.com/opencontainers/image-spec v1.1.0-rc5 // indirect github.com/opencontainers/runc v1.1.9 // indirect github.com/pelletier/go-toml v1.9.5 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.17.0 // indirect @@ -245,7 +253,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.31.0 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/exp v0.0.0-20250717185816-542afb5b7346 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/oauth2 v0.13.0 // indirect diff --git a/hgctl/go.sum b/hgctl/go.sum index 391fb1809..d176f5dfe 100644 --- a/hgctl/go.sum +++ b/hgctl/go.sum @@ -752,6 +752,7 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= github.com/chzyer/logex v1.1.11-0.20170329064859-445be9e134b2/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -910,6 +911,8 @@ github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyT github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/getkin/kin-openapi v0.118.0 h1:z43njxPmJ7TaPpMSCQb7PN0dEYno4tyBPQcrFdHoLuM= +github.com/getkin/kin-openapi v0.118.0/go.mod h1:l5e9PaFUo9fyLJCPGQeXI2ML8c3P8BHOEV2VaAVf/pc= github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= @@ -954,9 +957,10 @@ github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwds github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= -github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= @@ -987,8 +991,8 @@ github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/ github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= -github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-openapi/validate v0.18.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4= github.com/go-openapi/validate v0.19.2/go.mod h1:1tRCw7m3jtI8eNWEEliiAqUIcBztB2KDnRCRMUi7GTA= github.com/go-openapi/validate v0.19.5/go.mod h1:8DJv2CVJQ6kGNpFW6eV9N3JviE1C85nY1c2z52x1Gk4= @@ -1000,6 +1004,8 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gobuffalo/flect v0.2.0/go.mod h1:W3K3X9ksuZfir8f/LrfVtWmCDQFfayuylOJ7sz/Fj80= github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= @@ -1211,6 +1217,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.4 h1:7GHuZcgid37q8o5i3QI9KMT4nCWQQ3Kx3Ov github.com/hashicorp/golang-lru/v2 v2.0.4/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/higress-group/openapi-to-mcpserver v0.0.0-20250925065334-de60a170f950 h1:a3/hCNZednJoFbp1DPx2O/LRUwvcsyeTpL0MP+qIApg= +github.com/higress-group/openapi-to-mcpserver v0.0.0-20250925065334-de60a170f950/go.mod h1:jRTljni4fNs7aLiAbOhAAWIjctA4NSNtm5z7kGimG6U= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= @@ -1231,6 +1239,8 @@ github.com/in-toto/in-toto-golang v0.5.0/go.mod h1:/Rq0IZHLV7Ku5gielPT4wPHJfH1Gd github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= +github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c h1:EFWADU43GY2T7NIYYbIHWdrG2hRiWyGSHeON57ZADBE= github.com/istio/viper v1.3.3-0.20190515210538-2789fed3109c/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= @@ -1337,6 +1347,8 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= +github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= @@ -1425,6 +1437,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= @@ -1481,6 +1495,9 @@ github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/9 github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/perimeterx/marshmallow v1.1.4/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= @@ -1623,8 +1640,9 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -1634,8 +1652,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= @@ -1648,7 +1667,11 @@ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/ github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk= github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531 h1:Y/M5lygoNPKwVNLMPXgVfsRT40CSFKXCxuU8LoHySjs= github.com/tonistiigi/vt100 v0.0.0-20230623042737-f9a4f7ef6531/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= +github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= +github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= @@ -1972,6 +1995,7 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2534,6 +2558,7 @@ gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200121175148-a6ecf24a6d71/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= diff --git a/hgctl/pkg/agent/agent.go b/hgctl/pkg/agent/agent.go new file mode 100644 index 000000000..21b61f4a2 --- /dev/null +++ b/hgctl/pkg/agent/agent.go @@ -0,0 +1,46 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "io" + + "github.com/spf13/cobra" + cmdutil "k8s.io/kubectl/pkg/cmd/util" +) + +func NewAgentCmd() *cobra.Command { + agentCmd := &cobra.Command{ + Use: "agent", + Short: "start the interactive agent window", + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(handleAgentInvoke(cmd.OutOrStdout())) + }, + } + + return agentCmd +} + +func handleAgentInvoke(w io.Writer) error { + + return getAgent().Start() +} + +// Sub-Agent1: +// 1. Parse the url provided by user to MCP server configuration. +// 2. Publish the parsed MCP Server to Higress +func addPrequisiteSubAgent() error { + return nil +} diff --git a/hgctl/pkg/agent/base.go b/hgctl/pkg/agent/base.go new file mode 100644 index 000000000..24167c205 --- /dev/null +++ b/hgctl/pkg/agent/base.go @@ -0,0 +1,61 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "fmt" + "os" +) + +const ( + AgentBinaryName = "claude" + BinaryVersion = "0.1.0" + DevVersion = "dev" + NodeLeastVersion = 18 + AgentInstallCmd = "npm install -g @anthropic-ai/claude-code" + AgentReleasePage = "https://docs.claude.com/en/docs/claude-code/setup" +) + +// set up the core env +// 1. check if npm is installed +// 2. check the npm version +// 3. install hgctl-agent +func getAgent() *AgenticCore { + if !checkAgentInstallStatus() { + fmt.Println("⚠️ Prerequisites not satisfied. Exiting...") + // exit directly + os.Exit(1) + } + + return NewAgenticCore() +} + +func checkAgentInstallStatus() bool { + // TODO: Support cross-platform:windows + + if !checkNodeInstall() { + if err := promptNodeInstall(); err != nil { + return false + } + } + + if !checkAgentInstall() { + if err := promptAgentInstall(); err != nil { + return false + } + } + + return true +} diff --git a/hgctl/pkg/agent/core.go b/hgctl/pkg/agent/core.go new file mode 100644 index 000000000..c5127f5ce --- /dev/null +++ b/hgctl/pkg/agent/core.go @@ -0,0 +1,46 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "os" + "os/exec" +) + +type AgenticCore struct { +} + +func NewAgenticCore() *AgenticCore { + return &AgenticCore{} +} + +func (c *AgenticCore) run(args ...string) error { + cmd := exec.Command(AgentBinaryName, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + +} + +// ------- Initialization ------- +func (c *AgenticCore) Start() error { + return c.run(AgentBinaryName) +} + +// ------- MCP ------- +func (c *AgenticCore) AddMCPServer(name string, url string) error { + return c.run("mcp", "add", "--transport", HTTP, name, url) +} diff --git a/hgctl/pkg/agent/mcp.go b/hgctl/pkg/agent/mcp.go new file mode 100644 index 000000000..6843d3dd5 --- /dev/null +++ b/hgctl/pkg/agent/mcp.go @@ -0,0 +1,314 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "fmt" + "io" + "net" + "net/url" + "os" + "strings" + + "github.com/alibaba/higress/hgctl/pkg/agent/services" + "github.com/fatih/color" + "github.com/higress-group/openapi-to-mcpserver/pkg/models" + "github.com/spf13/cobra" + "github.com/spf13/viper" + cmdutil "k8s.io/kubectl/pkg/cmd/util" +) + +type MCPType string + +const ( + HTTP string = "http" + SSE string = "sse" + OPENAPI string = "openapi" + DIRECT_ROUTE string = "DIRECT_ROUTE" + OPEN_API string = "OPEN_API" + + HIGRESS_CONSOLE_URL = "higress-console-url" + HIGRESS_CONSOLE_USER = "higress-console-user" + HIGRESS_CONSOLE_PASSWORD = "higress-console-password" +) + +type MCPAddArg struct { + // higress console auth arg + baseURL string + hgUser string + hgPassword string + + name string + url string + transport string + spec string + scope string + noPublish bool + // TODO: support mcp env + // env string + +} + +type MCPAddHandler struct { + core *AgenticCore + arg MCPAddArg + w io.Writer +} + +func NewMCPCmd() *cobra.Command { + mcpCmd := &cobra.Command{ + Use: "mcp", + Short: "for the mcp management", + } + + mcpCmd.AddCommand(newMCPAddCmd()) + + return mcpCmd +} + +func newMCPAddCmd() *cobra.Command { + // parameter + arg := &MCPAddArg{} + + cmd := &cobra.Command{ + Use: "add [name]", + Short: "add mcp server including http and openapi", + Run: func(cmd *cobra.Command, args []string) { + arg.name = args[0] + resolveHigressConsoleAuth(arg) + cmdutil.CheckErr(handleAddMCP(cmd.OutOrStdout(), *arg)) + color.Cyan("Tip: Try doing 'kubectl port-forward' and add the server to the agent manually, if MCP Server connection failed") + }, + Args: cobra.ExactArgs(1), + } + + cmd.PersistentFlags().StringVarP(&arg.transport, "transport", "t", HTTP, "Determine the MCP Server's Type") + cmd.PersistentFlags().StringVarP(&arg.url, "url", "u", "", "MCP server URL") + cmd.PersistentFlags().StringVarP(&arg.scope, "scope", "s", "project", `Configuration scope (project or global)`) + cmd.PersistentFlags().StringVar(&arg.spec, "spec", "", "Specification of the openapi api") + cmd.PersistentFlags().BoolVar(&arg.noPublish, "no-publish", false, "If set then the mcp server will not be plubished to higress") + + flagHigressConsoleAuth(cmd, arg) + + return cmd +} + +func newHanlder(c *AgenticCore, arg MCPAddArg, w io.Writer) *MCPAddHandler { + return &MCPAddHandler{c, arg, w} +} + +func (h *MCPAddHandler) validateArg() error { + if !h.arg.noPublish { + if h.arg.baseURL == "" || h.arg.hgUser == "" || h.arg.hgPassword == "" { + fmt.Println("--higress-console-user, --higress-console-url, --higress-console-password must be provided") + return fmt.Errorf("invalid args") + } + } + return nil + +} + +func (h *MCPAddHandler) addHTTPMCP() error { + if err := h.core.AddMCPServer(h.arg.name, h.arg.url); err != nil { + return fmt.Errorf("mcp add failed: %w", err) + } + + if !h.arg.noPublish { + return publishToHigress(h.arg, nil) + } + return nil + +} + +// hgctl mcp add -t openapi --name test-name --spec openapi.json +func (h *MCPAddHandler) addOpenAPIMCP() error { + // fmt.Printf("get mcp server: %s openapi-spec-file: %s\n", h.arg.name, h.arg.spec) + config := h.parseOpenapiSpec() + + // fmt.Printf("get config struct: %v", config) + + // publish to higress + if err := publishToHigress(h.arg, config); err != nil { + return err + } + + // add mcp server to agent + gatewayIP, err := GetHigressGatewayServiceIP() + if err != nil { + color.Red( + "failed to add mcp server [%s] while getting higress-gateway ip due to: %v \n You may try to do port-forward and add it to agent manually", h.arg.name, err) + return err + } + mcpURL := fmt.Sprintf("http://%s/mcp-servers/%s", gatewayIP, h.arg.name) + return h.core.AddMCPServer(h.arg.name, mcpURL) +} + +func (h *MCPAddHandler) parseOpenapiSpec() *models.MCPConfig { + return parseOpenapi2MCP(h.arg) +} + +func handleAddMCP(w io.Writer, arg MCPAddArg) error { + client := getAgent() + h := newHanlder(client, arg, w) + if err := h.validateArg(); err != nil { + return err + } + + // spec -> OPENAPI + // noPublish -> typ + switch arg.transport { + case HTTP: + return h.addHTTPMCP() + case OPENAPI: + if arg.spec == "" { + return fmt.Errorf("--spec is required for openapi type") + } + if arg.noPublish { + return fmt.Errorf("--no-publish is not supported for openapi type") + } + if arg.url != "" { + return fmt.Errorf("--url is not supported for openapi type") + } + return h.addOpenAPIMCP() + default: + return fmt.Errorf("unsupported mcp type") + } +} + +func publishToHigress(arg MCPAddArg, config *models.MCPConfig) error { + // 1. parse the raw http url + // 2. add service source + // 3. add MCP server request + client := services.NewHigressClient(arg.baseURL, arg.hgUser, arg.hgPassword) + + // mcp server's url + rawURL := arg.url + // DIRECT_ROUTE or OPEN_API + mcpType := DIRECT_ROUTE + + if config != nil { + // TODO: here use tools's url directly, need to be considered + rawURL = config.Tools[0].RequestTemplate.URL + mcpType = OPEN_API + } + + res, err := url.Parse(rawURL) + if err != nil { + return err + } + + // add service source + srvType := "" + srvPort := "" + srvName := fmt.Sprintf("hgctl-%s", arg.name) + srvPath := res.Path + + if ip := net.ParseIP(res.Hostname()); ip == nil { + srvType = "dns" + } else { + srvType = "static" + } + + if res.Port() == "" && res.Scheme == "http" { + srvPort = "80" + } else if res.Port() == "" && res.Scheme == "https" { + srvPort = "443" + } else { + srvPort = res.Port() + } + + _, err = services.HandleAddServiceSource(client, map[string]interface{}{ + "domain": res.Host, + "type": srvType, + "port": srvPort, + "name": srvName, + "domainForEdit": res.Host, + "protocol": res.Scheme, + }) + if err != nil { + return err + } + + srvField := []map[string]interface{}{{ + "name": fmt.Sprintf("%s.%s", srvName, srvType), + "port": srvPort, + "version": "1.0", + "weight": 100, + }} + + // generete mcp server add request body + body := map[string]interface{}{ + "name": arg.name, + // "description": "", + "type": mcpType, + "service": fmt.Sprintf("%s.%s:%s", srvName, srvType, srvPort), + "upstreamPathPrefix": srvPath, + "services": srvField, + } + + // fmt.Printf("request body: %v", body) + + _, err = services.HandleAddMCPServer(client, body) + if err != nil { + return err + } + + if mcpType == OPEN_API { + addMCPToolConfig(client, config, srvField) + } + + return nil +} + +func addMCPToolConfig(client *services.HigressClient, config *models.MCPConfig, srvField []map[string]interface{}) { + body := map[string]interface{}{ + "name": config.Server.Name, + // "description": "", + "services": srvField, + "type": OPEN_API, + "rawConfigurations": convertMCPConfigToStr(config), + "mcpServerName": config.Server.Name, + } + + _, err := services.HandleAddOpenAPITool(client, body) + if err != nil { + fmt.Printf("add openapi tools failed: %v\n", err) + os.Exit(1) + } + // fmt.Println("get openapi tools add response: ", string(resp)) +} + +func flagHigressConsoleAuth(cmd *cobra.Command, arg *MCPAddArg) { + cmd.PersistentFlags().StringVar(&arg.baseURL, HIGRESS_CONSOLE_URL, "", "The BaseURL of higress console") + cmd.PersistentFlags().StringVar(&arg.hgUser, HIGRESS_CONSOLE_USER, "", "The username of higress console") + cmd.PersistentFlags().StringVarP(&arg.hgPassword, HIGRESS_CONSOLE_PASSWORD, "p", "", "The password of higress console") + + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) + viper.AutomaticEnv() + // TODO: if higress is installed by hgctl, then try to resolve auth arg in install profile +} + +// resolve from viper +func resolveHigressConsoleAuth(arg *MCPAddArg) { + if arg.baseURL == "" { + arg.baseURL = viper.GetString(HIGRESS_CONSOLE_URL) + } + if arg.hgUser == "" { + arg.hgUser = viper.GetString(HIGRESS_CONSOLE_USER) + } + if arg.hgPassword == "" { + arg.hgPassword = viper.GetString(HIGRESS_CONSOLE_PASSWORD) + } +} diff --git a/hgctl/pkg/agent/services/client.go b/hgctl/pkg/agent/services/client.go new file mode 100644 index 000000000..01525b988 --- /dev/null +++ b/hgctl/pkg/agent/services/client.go @@ -0,0 +1,113 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package services + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type HigressClient struct { + baseURL string + username string + password string + httpClient *http.Client +} + +func NewHigressClient(baseURL, username, password string) *HigressClient { + client := &HigressClient{ + baseURL: baseURL, + username: username, + password: password, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } + + return client +} + +func (c *HigressClient) Get(path string) ([]byte, error) { + return c.request("GET", path, nil) +} + +func (c *HigressClient) Post(path string, data interface{}) ([]byte, error) { + return c.request("POST", path, data) +} + +func (c *HigressClient) Put(path string, data interface{}) ([]byte, error) { + return c.request("PUT", path, data) +} + +func (c *HigressClient) Delete(path string) ([]byte, error) { + return c.request("DELETE", path, nil) +} +func (c *HigressClient) request(method, path string, data interface{}) ([]byte, error) { + url := c.baseURL + path + + var body io.Reader + if data != nil { + jsonData, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("failed to marshal request data: %w", err) + } + body = bytes.NewBuffer(jsonData) + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, method, url, body) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.SetBasicAuth(c.username, c.password) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode == 409 { + return nil, fmt.Errorf("resource already exists") + } + + if resp.StatusCode == 400 { + return nil, fmt.Errorf("invalid resource definition") + } + + if resp.StatusCode == 500 { + return nil, fmt.Errorf("server internal error") + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return nil, fmt.Errorf("HTTP error %d", resp.StatusCode) + } + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + return respBody, nil +} diff --git a/hgctl/pkg/agent/services/service.go b/hgctl/pkg/agent/services/service.go new file mode 100644 index 000000000..ec9a5ada9 --- /dev/null +++ b/hgctl/pkg/agent/services/service.go @@ -0,0 +1,129 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package services + +import ( + "fmt" +) + +func HandleAddServiceSource(client *HigressClient, body interface{}) ([]byte, error) { + data, ok := body.(map[string]interface{}) + // fmt.Printf("request body: %v\n", data) + if !ok { + return nil, fmt.Errorf("failed to parse request body") + } + // Validate + if _, ok := data["name"]; !ok { + return nil, fmt.Errorf("missing required field 'name' in body") + } + if _, ok := data["type"]; !ok { + return nil, fmt.Errorf("missing required field 'type' in body") + } + if _, ok := data["domain"]; !ok { + return nil, fmt.Errorf("missing required field 'domain' in body") + } + if _, ok := data["port"]; !ok { + return nil, fmt.Errorf("missing required field 'port' in body") + } + + resp, err := client.Post("/v1/service-sources", data) + if err != nil { + return nil, fmt.Errorf("failed to add service source: %w", err) + } + // res := make(map[string]interface{}) + + return resp, nil +} + +// add MCP server to higress console, example request body as followed: +// +// { +// "name": "mcp-deepwiki", +// "description": "", +// "type": "DIRECT_ROUTE", // or OPEN_API +// "service": "hgctl-deepwiki.dns:443", +// "upstreamPathPrefix": "/mcp", +// "services": [ +// { +// "name": "hgctl-deepwiki.dns", +// "port": 443, +// "version": "1.0", +// "weight": 100 +// } +// ] +// } +func HandleAddMCPServer(client *HigressClient, body interface{}) ([]byte, error) { + data, ok := body.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("failed to parse request body") + } + // Validate + if _, ok := data["name"]; !ok { + return nil, fmt.Errorf("missing required field 'name' in body") + } + if _, ok := data["type"]; !ok { + return nil, fmt.Errorf("missing required field 'type' in body") + } + if _, ok := data["service"]; !ok { + return nil, fmt.Errorf("missing required field 'service' in body") + } + + // if _, ok := data["upstreamPathPrefix"]; !ok { + // return nil, fmt.Errorf("missing required field 'upstreamPathPrefix' in body") + // } + + _, ok = data["services"] + if !ok { + return nil, fmt.Errorf("missing required field 'port' in body") + } + + resp, err := client.Put("/v1/mcpServer", data) + if err != nil { + return nil, fmt.Errorf("failed to add mcp server: %w", err) + } + + return resp, nil +} + +// add OpenAPI MCP tools to higress console, example request body: +// +// { +// "id": null, +// "name": "openapi-name", +// "description": "123", +// "domains": [], +// "services": [ +// { +// "name": "kubernetes.default.svc.cluster.local", +// "port": 443, +// "version": null, +// "weight": 100 +// } +// ], +// "type": "OPEN_API", +// "consumerAuthInfo": { +// "type": "key-auth", +// "enable": false, +// "allowedConsumers": [] +// }, +// "rawConfigurations": "", // MCP configuration str +// "dsn": null, +// "dbType": null, +// "upstreamPathPrefix": null, +// "mcpServerName": "openapi-name" +// } +func HandleAddOpenAPITool(client *HigressClient, body interface{}) ([]byte, error) { + return client.Put("/v1/mcpServer", body) +} diff --git a/hgctl/pkg/agent/types.go b/hgctl/pkg/agent/types.go new file mode 100644 index 000000000..6a3fe3b96 --- /dev/null +++ b/hgctl/pkg/agent/types.go @@ -0,0 +1,134 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +type Message struct { + Role string `json:"role"` + Content string `json:"content"` +} + +type Request struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + FrequencyPenalty float64 `json:"frequency_penalty"` + PresencePenalty float64 `json:"presence_penalty"` + Stream bool `json:"stream"` + Temperature float64 `json:"temperature"` + Topp int32 `json:"top_p"` +} + +type Choice struct { + Index int `json:"index"` + Message Message `json:"message"` + FinishReason string `json:"finish_reason"` +} + +type Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` +} + +type Response struct { + ID string `json:"id"` + Choices []Choice `json:"choices"` + Created int64 `json:"created"` + Model string `json:"model"` + Object string `json:"object"` + Usage Usage `json:"usage"` +} + +type ToolsParam struct { + ToolName string `yaml:"toolName"` + Path string `yaml:"path"` + Method string `yaml:"method"` + ParamName []string `yaml:"paramName"` + Parameter string `yaml:"parameter"` + Description string `yaml:"description"` +} + +type Info struct { + Title string `yaml:"title"` + Description string `yaml:"description"` + Version string `yaml:"version"` +} + +type Server struct { + URL string `yaml:"url"` +} + +type Parameter struct { + Name string `yaml:"name"` + In string `yaml:"in"` + Description string `yaml:"description"` + Required bool `yaml:"required"` + Schema struct { + Type string `yaml:"type"` + Default string `yaml:"default"` + Enum []string `yaml:"enum"` + } `yaml:"schema"` +} + +type Items struct { + Type string `yaml:"type"` + Example string `yaml:"example"` +} + +type Property struct { + Description string `yaml:"description"` + Type string `yaml:"type"` + Enum []string `yaml:"enum,omitempty"` + Items *Items `yaml:"items,omitempty"` + MaxItems int `yaml:"maxItems,omitempty"` + Example string `yaml:"example,omitempty"` +} + +type Schema struct { + Type string `yaml:"type"` + Required []string `yaml:"required"` + Properties map[string]Property `yaml:"properties"` +} + +type MediaType struct { + Schema Schema `yaml:"schema"` +} + +type RequestBody struct { + Required bool `yaml:"required"` + Content map[string]MediaType `yaml:"content"` +} + +type PathItem struct { + Description string `yaml:"description"` + Summary string `yaml:"summary"` + OperationID string `yaml:"operationId"` + RequestBody RequestBody `yaml:"requestBody"` + Parameters []Parameter `yaml:"parameters"` + Deprecated bool `yaml:"deprecated"` +} + +type Paths map[string]map[string]PathItem + +type Components struct { + Schemas map[string]interface{} `yaml:"schemas"` +} + +type API struct { + OpenAPI string `yaml:"openapi"` + Info Info `yaml:"info"` + Servers []Server `yaml:"servers"` + Paths Paths `yaml:"paths"` + Components Components `yaml:"components"` +} diff --git a/hgctl/pkg/agent/utils.go b/hgctl/pkg/agent/utils.go new file mode 100644 index 000000000..1162eaf7d --- /dev/null +++ b/hgctl/pkg/agent/utils.go @@ -0,0 +1,455 @@ +// Copyright (c) 2025 Alibaba Group Holding Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package agent + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/braydonk/yaml" + "github.com/fatih/color" + "github.com/higress-group/openapi-to-mcpserver/pkg/converter" + "github.com/higress-group/openapi-to-mcpserver/pkg/models" + "github.com/higress-group/openapi-to-mcpserver/pkg/parser" + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "github.com/spf13/viper" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8s "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +var binaryName = AgentBinaryName + +// ------ cmd related ------ +func BindFlagToEnv(cmd *cobra.Command, flagName, envName string) { + _ = viper.BindPFlag(flagName, cmd.PersistentFlags().Lookup(flagName)) + _ = viper.BindEnv(flagName, envName) +} + +// ------ Prompt to install prequisite environment ------ +func checkNodeInstall() bool { + cmd := exec.Command("node", "-v") + out, err := cmd.Output() + if err != nil { + return false + } + + versionStr := strings.TrimPrefix(strings.TrimSpace(string(out)), "v") + parts := strings.Split(versionStr, ".") + if len(parts) == 0 { + return false + } + + major, err := strconv.Atoi(parts[0]) + if err != nil { + return false + } + + return major >= NodeLeastVersion +} + +func promptNodeInstall() error { + fmt.Println() + color.Yellow("⚠️ Node.js is not installed or not found in PATH.") + color.Cyan("🔧 Node.js is required to run the agent.") + fmt.Println() + + options := []string{ + "🚀 Install automatically (recommended)", + "📖 Exit and show manual installation guide", + } + + var ans string + prompt := &survey.Select{ + Message: "How would you like to install Node.js?", + Options: options, + } + if err := survey.AskOne(prompt, &ans); err != nil { + return fmt.Errorf("selection error: %w", err) + } + + switch ans { + case "🚀 Install automatically (recommended)": + fmt.Println() + color.Green("🚀 Installing Node.js automatically...") + + if err := installNodeAutomatically(); err != nil { + color.Red("❌ Installation failed: %v", err) + fmt.Println() + showNodeManualInstallation() + return errors.New("node.js installation failed") + } + + color.Green("✅ Node.js installation completed!") + fmt.Println() + color.Blue("🔍 Verifying installation...") + + if checkNodeInstall() { + color.Green("🎉 Node.js is now available!") + return nil + } else { + color.Yellow("⚠️ Node.js installation completed but not found in PATH.") + color.Cyan("💡 You may need to restart your terminal or source your shell profile.") + return errors.New("node.js installed but not in PATH") + } + + case "📖 Exit and show manual installation guide": + showNodeManualInstallation() + return errors.New("node.js not installed") + + default: + return errors.New("invalid selection") + } +} + +func installNodeAutomatically() error { + switch runtime.GOOS { + case "windows": + color.Cyan("📦 Please download Node.js installer from https://nodejs.org and run it manually on Windows") + return errors.New("automatic installation not supported on Windows yet") + case "darwin": + // macOS: use brew + cmd := exec.Command("brew", "install", "node") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + case "linux": + // Linux (Debian/Ubuntu example) + cmd := exec.Command("sudo", "apt", "update") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return err + } + cmd = exec.Command("sudo", "apt", "install", "-y", "nodejs", "npm") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + default: + return errors.New("unsupported OS for automatic installation") + } +} + +func showNodeManualInstallation() { + fmt.Println() + + color.New(color.FgGreen, color.Bold).Println("📖 Manual Node.js Installation Guide") + fmt.Println() + + fmt.Println(color.MagentaString("Choose one of the following installation methods:")) + fmt.Println() + + color.Cyan("Method 1: Install via package manager") + color.Cyan("macOS (brew): brew install node") + color.Cyan("Ubuntu/Debian: sudo apt install -y nodejs npm") + color.Cyan("Windows: download from https://nodejs.org and run installer") + fmt.Println() + + color.Yellow("Method 2: Download from official website") + color.Yellow("1. Download Node.js from https://nodejs.org/en/download/") + color.Yellow("2. Follow installer instructions and add to PATH if needed") + fmt.Println() + + color.Green("✅ Verify Installation") + fmt.Println(color.WhiteString("node -v")) + fmt.Println(color.WhiteString("npm -v")) + fmt.Println() + + color.Cyan("💡 After installation, restart your terminal or source your shell profile.") + fmt.Println() +} + +func checkAgentInstall() bool { + cmd := exec.Command(binaryName, "--version") + if err := cmd.Run(); err != nil { + return false + } + return true +} + +func promptAgentInstall() error { + fmt.Println() + color.Yellow("⚠️ %s is not installed or not found in PATH.", binaryName) + color.Cyan("🔧 %s is required to run the agent.", binaryName) + fmt.Println() + + options := []string{ + "🚀 Install automatically (recommended)", + "📖 Exit and show manual installation guide", + } + + var ans string + prompt := &survey.Select{ + Message: "How would you like to install " + binaryName + "?", + Options: options, + } + if err := survey.AskOne(prompt, &ans); err != nil { + return fmt.Errorf("selection error: %w", err) + } + + switch ans { + case "🚀 Install automatically (recommended)": + fmt.Println() + color.Green("🚀 Installing %s automatically...", binaryName) + + if err := installAgentAutomatically(); err != nil { + color.Red("❌ Installation failed: %v", err) + fmt.Println() + showAgentManualInstallation() + return errors.New(binaryName + " installation failed") + } + + color.Green("✅ %s installation completed!", binaryName) + fmt.Println() + color.Blue("🔍 Verifying installation...") + + if checkAgentInstall() { + color.Green("🎉 %s is now available!", binaryName) + return nil + } else { + color.Yellow("⚠️ %s installed but not found in PATH.", binaryName) + color.Cyan("💡 You may need to restart your terminal or source your shell profile.") + return errors.New(binaryName + " installed but not in PATH") + } + + case "📖 Exit and show manual installation guide": + showAgentManualInstallation() + return errors.New(binaryName + " not installed") + + default: + return errors.New("invalid selection") + } +} + +func installAgentAutomatically() error { + switch runtime.GOOS { + case "windows": + cmd := exec.Command("cmd", "/C", AgentInstallCmd) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + case "darwin": + cmd := exec.Command("bash", "-c", AgentInstallCmd) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + case "linux": + cmd := exec.Command("bash", "-c", AgentInstallCmd) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() + default: + return errors.New("unsupported OS for automatic installation") + } +} + +func showAgentManualInstallation() { + fmt.Println() + color.New(color.FgGreen, color.Bold).Printf("📖 Manual %s Installation Guide\n", binaryName) + fmt.Println() + + fmt.Println(color.MagentaString("Supported Operating Systems: macOS 10.15+, Ubuntu 20.04+/Debian 10+, or Windows 10+ (WSL/Git for Windows)")) + fmt.Println(color.MagentaString("Hardware: 4GB+ RAM")) + fmt.Println(color.MagentaString("Software: Node.js 18+")) + fmt.Println(color.MagentaString("Network: Internet connection required for authentication and AI processing")) + fmt.Println(color.MagentaString("Shell: Works best in Bash, Zsh, or Fish")) + fmt.Println() + + color.Cyan("Method 1: Download prebuilt binary") + color.Cyan(fmt.Sprintf("1. Go to official release page: %s", AgentReleasePage)) + fmt.Printf(color.CyanString("2. Download %s for your OS\n"), binaryName) + color.Cyan("3. Make it executable and place it in a directory in your PATH") + fmt.Println() + + fmt.Println() + color.Green("✅ Verify Installation") + fmt.Printf(color.WhiteString("%s --version\n"), binaryName) + fmt.Println() + color.Cyan("💡 After installation, restart your terminal or source your shell profile.") + fmt.Println() +} + +// ------ MCP convert utils function ------ +func parseOpenapi2MCP(arg MCPAddArg) *models.MCPConfig { + path := arg.spec + serverName := arg.name + + // Create a new parser + p := parser.NewParser() + + p.SetValidation(true) + + // Parse the OpenAPI specification + err := p.ParseFile(path) + if err != nil { + fmt.Printf("Error parsing OpenAPI specification: %v\n", err) + os.Exit(1) + } + + c := converter.NewConverter(p, models.ConvertOptions{ + ServerName: serverName, + ToolNamePrefix: "", + TemplatePath: "", + }) + + // Convert the OpenAPI specification to an MCP configuration + config, err := c.Convert() + if err != nil { + fmt.Printf("Error converting OpenAPI specification: %v\n", err) + os.Exit(1) + } + + return config +} + +func convertMCPConfigToStr(cfg *models.MCPConfig) string { + var data []byte + var buffer bytes.Buffer + encoder := yaml.NewEncoder(&buffer) + encoder.SetIndent(2) + + if err := encoder.Encode(cfg); err != nil { + fmt.Printf("Error encoding YAML: %v\n", err) + os.Exit(1) + } + data = buffer.Bytes() + str := string(data) + + // fmt.Println("Successfully converted OpenAPI specification to MCP Server") + // fmt.Printf("Get MCP server config string: %v", str) + return str + + // if err != nil { + // fmt.Printf("Error marshaling MCP configuration: %v\n", err) + // os.Exit(1) + // } + + // err = os.WriteFile(*outputFile, data, 0644) + // if err != nil { + // fmt.Printf("Error writing MCP configuration: %v\n", err) + // os.Exit(1) + // } + +} + +func GetHigressGatewayServiceIP() (string, error) { + color.Cyan("🚀 Adding openapi MCP Server to agent, checking Higress Gateway Pod status...") + + defaultKubeconfig := filepath.Join(os.Getenv("HOME"), ".kube", "config") + config, err := clientcmd.BuildConfigFromFlags("", defaultKubeconfig) + if err != nil { + color.Yellow("⚠️ Failed to load default kubeconfig: %v", err) + return promptForServiceKubeSettingsAndRetry() + } + + clientset, err := k8s.NewForConfig(config) + if err != nil { + color.Yellow("⚠️ Failed to create Kubernetes client: %v", err) + return promptForServiceKubeSettingsAndRetry() + } + + namespace := "higress-system" + svc, err := clientset.CoreV1().Services(namespace).Get(context.Background(), "higress-gateway", metav1.GetOptions{}) + if err != nil || svc == nil { + color.Yellow("⚠️ Could not find Higress Gateway Service in namespace '%s'.", namespace) + return promptForServiceKubeSettingsAndRetry() + } + + ip, err := extractServiceIP(clientset, namespace, svc) + if err != nil { + return "", err + } + + color.Green("✅ Found Higress Gateway Service IP: %s (namespace: %s)", ip, namespace) + return ip, nil +} + +// higress-gateway should always be LoadBalancer +func extractServiceIP(clientset *k8s.Clientset, namespace string, svc *v1.Service) (string, error) { + return svc.Spec.ClusterIP, nil + + // // fallback to Pod IP + // if len(svc.Spec.Selector) > 0 { + // selector := metav1.FormatLabelSelector(&metav1.LabelSelector{MatchLabels: svc.Spec.Selector}) + // pods, err := clientset.CoreV1().Pods(namespace).List(context.Background(), metav1.ListOptions{ + // LabelSelector: selector, + // }) + // if err != nil { + // return "", fmt.Errorf("failed to list pods for selector: %v", err) + // } + // if len(pods.Items) > 0 { + // return pods.Items[0].Status.PodIP, nil + // } + // } + +} + +// prompt fallback for user input +func promptForServiceKubeSettingsAndRetry() (string, error) { + color.Cyan("Let's fix it manually 👇") + + kubeconfigPrompt := promptui.Prompt{ + Label: "Enter kubeconfig path", + Default: filepath.Join(os.Getenv("HOME"), ".kube", "config"), + } + kubeconfigPath, err := kubeconfigPrompt.Run() + if err != nil { + return "", fmt.Errorf("aborted: %v", err) + } + + nsPrompt := promptui.Prompt{ + Label: "Enter Higress namespace", + Default: "higress-system", + } + namespace, err := nsPrompt.Run() + if err != nil { + return "", err + } + + config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) + if err != nil { + return "", fmt.Errorf("failed to load kubeconfig: %v", err) + } + + clientset, err := k8s.NewForConfig(config) + if err != nil { + return "", fmt.Errorf("failed to create kubernetes client: %v", err) + } + + svc, err := clientset.CoreV1().Services(namespace).Get(context.Background(), "higress-gateway", metav1.GetOptions{}) + if err != nil || svc == nil { + color.Red("❌ Higress Gateway Service not found in namespace '%s'", namespace) + return "", fmt.Errorf("service not found") + } + + ip, err := extractServiceIP(clientset, namespace, svc) + if err != nil { + return "", err + } + + color.Green("✅ Found Higress Gateway Service IP: %s (namespace: %s)", ip, namespace) + return ip, nil +} diff --git a/hgctl/pkg/root.go b/hgctl/pkg/root.go index ef4a507bb..9229f8b94 100644 --- a/hgctl/pkg/root.go +++ b/hgctl/pkg/root.go @@ -17,6 +17,7 @@ package hgctl import ( "os" + "github.com/alibaba/higress/hgctl/pkg/agent" "github.com/alibaba/higress/hgctl/pkg/plugin" "github.com/spf13/cobra" ) @@ -42,6 +43,8 @@ func GetRootCommand() *cobra.Command { rootCmd.AddCommand(plugin.NewCommand()) rootCmd.AddCommand(newCompletionCmd(os.Stdout)) rootCmd.AddCommand(newCodeDebugCmd()) + rootCmd.AddCommand(agent.NewMCPCmd()) + rootCmd.AddCommand(agent.NewAgentCmd()) return rootCmd } diff --git a/tools/hack/get-hgctl.sh b/tools/hack/get-hgctl.sh index 57867e1c9..b77f7916b 100644 --- a/tools/hack/get-hgctl.sh +++ b/tools/hack/get-hgctl.sh @@ -23,6 +23,10 @@ export VERSION HAS_CURL="$(type "curl" &>/dev/null && echo true || echo false)" HAS_WGET="$(type "wget" &>/dev/null && echo true || echo false)" HAS_GIT="$(type "git" &>/dev/null && echo true || echo false)" +HAS_NODE="$(type "node" &>/dev/null && echo true || echo false)" + +# the lowest node version required +REQUIRED_NODE_VERSION="20.18.1" # initArch discovers the architecture for this system. initArch() { @@ -76,8 +80,121 @@ verifySupported() { if [ "${HAS_GIT}" != "true" ]; then echo "[WARNING] Could not find git. It is required for plugin installation." fi + + if [ "${HAS_NODE}" != "true" ]; then + echo "[ERROR] Could not find node. It is required for hgctl agent support." + echo "Node.js >= ${REQUIRED_NODE_VERSION} is required." + echo "Start to install node..." + installNode + else + checkNodeVersion + fi + } +checkNodeVersion() { + local current_version=$(node -v | sed 's/v//') + + if ! verifyNodeVersion "$current_version" "$REQUIRED_NODE_VERSION"; then + echo "[ERROR] Node.js version $current_version is installed, but >= ${REQUIRED_NODE_VERSION} is required." + echo "Please upgrade Node.js or install a newer version." + echo "Visit: https://nodejs.org/ or use nvm: https://github.com/nvm-sh/nvm" + exit 1 + else + echo "[INFO] Node.js version $current_version meets the requirement (>= ${REQUIRED_NODE_VERSION})" + fi +} + +verifyNodeVersion() { + local current=$1 + local required=$2 + + local current_major=$(echo "$current" | cut -d. -f1) + local current_minor=$(echo "$current" | cut -d. -f2) + local current_patch=$(echo "$current" | cut -d. -f3) + + local required_major=$(echo "$required" | cut -d. -f1) + local required_minor=$(echo "$required" | cut -d. -f2) + local required_patch=$(echo "$required" | cut -d. -f3) + + if [ "$current_major" -gt "$required_major" ]; then + return 0 + elif [ "$current_major" -lt "$required_major" ]; then + return 1 + fi + + if [ "$current_minor" -gt "$required_minor" ]; then + return 0 + elif [ "$current_minor" -lt "$required_minor" ]; then + return 1 + fi + + if [ "$current_patch" -ge "$required_patch" ]; then + return 0 + else + return 1 + fi +} + +installNode() { + echo "Installing Node.js ${REQUIRED_NODE_VERSION}..." + + case "$OS" in + darwin) + installNodeMacOS + ;; + linux) + installNodeLinux + ;; + windows) + installNodeWindows + ;; + *) + echo "[ERROR] Unsupported OS: $OS" + echo "Please install Node.js manually from https://nodejs.org/" + exit 1 + ;; + esac +} + +installNodeMacOS() { + if type "brew" &>/dev/null; then + echo "Using Homebrew to install Node.js..." + brew install node@20 + else + echo "[ERROR] Homebrew not found. Please install Homebrew first:" + echo " /bin/bash -c \\"\\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\\"" + echo "Or install Node.js manually from https://nodejs.org/" + exit 1 + fi +} + +installNodeLinux() { + echo "Installing Node.js via NodeSource repository..." + + if [ "${HAS_CURL}" == "true" ]; then + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + sudo apt-get install -y nodejs + elif [ "${HAS_WGET}" == "true" ]; then + wget -qO- https://deb.nodesource.com/setup_20.x | sudo -E bash - + sudo apt-get install -y nodejs + else + echo "[ERROR] Neither curl nor wget found. Cannot install Node.js." + echo "Please install Node.js manually from https://nodejs.org/" + exit 1 + fi +} + +installNodeWindows() { + echo "[ERROR] Automatic Node.js installation on Windows is not supported." + echo "Please download and install Node.js manually from:" + echo " https://nodejs.org/dist/v${REQUIRED_NODE_VERSION}/node-v${REQUIRED_NODE_VERSION}-x64.msi" + echo "Or use a package manager like Chocolatey:" + echo " choco install nodejs --version=${REQUIRED_NODE_VERSION}" + exit 1 +} + + # checkDesiredVersion checks if the desired version is available. checkDesiredVersion() { if [ "$VERSION" == "" ]; then @@ -209,4 +326,4 @@ if ! checkhgctlInstalledVersion; then fi fi testVersion -cleanup +cleanup \ No newline at end of file