mirror of
https://github.com/alibaba/higress.git
synced 2026-02-26 05:30:50 +08:00
514 lines
15 KiB
Go
514 lines
15 KiB
Go
// 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/alibaba/higress/hgctl/pkg/helm"
|
|
"github.com/alibaba/higress/hgctl/pkg/installer"
|
|
"github.com/alibaba/higress/hgctl/pkg/kubernetes"
|
|
"github.com/alibaba/higress/v2/pkg/cmd/options"
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
SecretConsoleUser = "adminUsername"
|
|
SecretConsolePwd = "adminPassword"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
func getConsoleCredentials(profile *helm.Profile) (username, password string, err error) {
|
|
cliClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
|
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("failed to build kubernetes client: %w", err)
|
|
}
|
|
|
|
secret, err := cliClient.KubernetesInterface().CoreV1().Secrets(profile.Global.Namespace).Get(context.Background(), "higress-console", metav1.GetOptions{})
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return string(secret.Data[SecretConsoleUser]), string(secret.Data[SecretConsolePwd]), nil
|
|
}
|
|
|
|
// This function will do following things:
|
|
// 1. read the profile from local-file
|
|
// 2. read the profile from k8s' configMap
|
|
// 3. combine the two type profiles together and return
|
|
func getAllProfiles() ([]*installer.ProfileContext, error) {
|
|
profileContexts := make([]*installer.ProfileContext, 0)
|
|
profileInstalledPath, err := installer.GetProfileInstalledPath()
|
|
if err != nil {
|
|
return profileContexts, nil
|
|
}
|
|
fileProfileStore, err := installer.NewFileDirProfileStore(profileInstalledPath)
|
|
if err != nil {
|
|
return profileContexts, nil
|
|
}
|
|
fileProfileContexts, err := fileProfileStore.List()
|
|
if err == nil {
|
|
profileContexts = append(profileContexts, fileProfileContexts...)
|
|
}
|
|
|
|
cliClient, err := kubernetes.NewCLIClient(options.DefaultConfigFlags.ToRawKubeConfigLoader())
|
|
if err != nil {
|
|
return profileContexts, nil
|
|
}
|
|
configmapProfileStore, err := installer.NewConfigmapProfileStore(cliClient)
|
|
if err != nil {
|
|
return profileContexts, nil
|
|
}
|
|
|
|
configmapProfileContexts, err := configmapProfileStore.List()
|
|
if err == nil {
|
|
profileContexts = append(profileContexts, configmapProfileContexts...)
|
|
}
|
|
return profileContexts, nil
|
|
}
|