diff --git a/go.mod b/go.mod index 482f0f43c61a..8e4d2221c450 100644 --- a/go.mod +++ b/go.mod @@ -76,6 +76,7 @@ require ( github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect + github.com/imdario/mergo v0.3.6 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect @@ -96,6 +97,7 @@ require ( github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/scram v1.1.2 // indirect diff --git a/go.sum b/go.sum index 3bf2a8444633..6f6d37828fee 100644 --- a/go.sum +++ b/go.sum @@ -90,6 +90,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= diff --git a/zrpc/resolver/internal/kubebuilder.go b/zrpc/resolver/internal/kubebuilder.go index 9d63e8c4bfcc..3e5b599b4988 100644 --- a/zrpc/resolver/internal/kubebuilder.go +++ b/zrpc/resolver/internal/kubebuilder.go @@ -4,7 +4,11 @@ package internal import ( "context" + "errors" "fmt" + "os" + "path/filepath" + "strings" "time" "github.com/zeromicro/go-zero/core/logx" @@ -15,11 +19,14 @@ import ( "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" ) const ( - resyncInterval = 5 * time.Minute - serviceSelector = "kubernetes.io/service-name=" + resyncInterval = 5 * time.Minute + serviceSelector = "kubernetes.io/service-name=" + localFallbackEnvKey = "GOZERO_K8S_LOCAL_FALLBACK" + localFallbackEnvValueTrue = "true" ) type kubeResolver struct { @@ -51,7 +58,28 @@ func (b *kubeBuilder) Build(target resolver.Target, cc resolver.ClientConn, config, err := rest.InClusterConfig() if err != nil { - return nil, err + // Check if local fallback is enabled via environment variable + if !strings.EqualFold(os.Getenv(localFallbackEnvKey), localFallbackEnvValueTrue) { + return nil, fmt.Errorf("not running in cluster and %s is not set to true: %w", + localFallbackEnvKey, err) + } + + // Try to load kubeconfig from KUBECONFIG env or default path + kubeconfig := os.Getenv("KUBECONFIG") + if kubeconfig == "" { + home, errHome := os.UserHomeDir() + if errHome != nil { + return nil, errors.Join(err, errHome) + } + kubeconfig = filepath.Join(home, ".kube", "config") + } + + localConfig, errLocal := clientcmd.BuildConfigFromFlags("", kubeconfig) + if errLocal != nil { + return nil, fmt.Errorf("k8s config load failed from %s: %w", kubeconfig, + errors.Join(err, errLocal)) + } + config = localConfig } cs, err := kubernetes.NewForConfig(config) diff --git a/zrpc/resolver/internal/kubebuilder_test.go b/zrpc/resolver/internal/kubebuilder_test.go index bc9750dd9c77..57586926c4ca 100644 --- a/zrpc/resolver/internal/kubebuilder_test.go +++ b/zrpc/resolver/internal/kubebuilder_test.go @@ -15,20 +15,94 @@ func TestKubeBuilder_Scheme(t *testing.T) { } func TestKubeBuilder_Build(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + t.Setenv("KUBERNETES_SERVICE_HOST", "") + t.Setenv("KUBERNETES_SERVICE_PORT", "") + t.Setenv("GOZERO_K8S_LOCAL_FALLBACK", "true") + var b kubeBuilder - u, err := url.Parse(fmt.Sprintf("%s://%s", KubernetesScheme, "a,b")) - assert.NoError(t, err) + cc := &mockedClientConn{} + + tests := []struct { + name string + input string + }{ + { + name: "invalid host", + input: fmt.Sprintf("%s://%s", KubernetesScheme, "a,b"), + }, + { + name: "bad endpoint format", + input: fmt.Sprintf("%s://%s:9100/a:b:c", KubernetesScheme, "a,b,c,d"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u, err := url.Parse(tt.input) + assert.NoError(t, err) + + _, err = b.Build(resolver.Target{ + URL: *u, + }, cc, resolver.BuildOptions{}) + assert.Error(t, err) + }) + } +} + +func TestKubeBuilder_Build_LocalFallback(t *testing.T) { + tests := []struct { + name string + fallbackEnv string + kubeconfig string + errContains string + }{ + { + name: "disabled when env not set", + fallbackEnv: "", + errContains: "GOZERO_K8S_LOCAL_FALLBACK", + }, + { + name: "disabled when env is false", + fallbackEnv: "false", + errContains: "GOZERO_K8S_LOCAL_FALLBACK", + }, + { + name: "enabled when env is true", + fallbackEnv: "true", + errContains: "k8s config load failed", + }, + { + name: "use custom KUBECONFIG path", + fallbackEnv: "true", + kubeconfig: "/nonexistent/custom/kubeconfig", + errContains: "/nonexistent/custom/kubeconfig", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + t.Setenv("KUBERNETES_SERVICE_HOST", "") + t.Setenv("KUBERNETES_SERVICE_PORT", "") + if tt.fallbackEnv != "" { + t.Setenv("GOZERO_K8S_LOCAL_FALLBACK", tt.fallbackEnv) + } + if tt.kubeconfig != "" { + t.Setenv("KUBECONFIG", tt.kubeconfig) + } - _, err = b.Build(resolver.Target{ - URL: *u, - }, nil, resolver.BuildOptions{}) - assert.Error(t, err) + var b kubeBuilder + cc := &mockedClientConn{} - u, err = url.Parse(fmt.Sprintf("%s://%s:9100/a:b:c", KubernetesScheme, "a,b,c,d")) - assert.NoError(t, err) + u, err := url.Parse(fmt.Sprintf("%s://my-service.default:8080", KubernetesScheme)) + assert.NoError(t, err) - _, err = b.Build(resolver.Target{ - URL: *u, - }, nil, resolver.BuildOptions{}) - assert.Error(t, err) + _, err = b.Build(resolver.Target{ + URL: *u, + }, cc, resolver.BuildOptions{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.errContains) + }) + } }