Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(fqdn): support using short name access service #23

Merged
merged 5 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 46 additions & 7 deletions core/manager/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package manager
import (
"fmt"
"os"
"strings"
"time"

"github.com/cloudwego/kitex/pkg/klog"
Expand All @@ -30,6 +31,7 @@ import (
const (
PodNamespace = "POD_NAMESPACE"
PodName = "POD_NAME"
MetaNamespace = "NAMESPACE"
InstanceIP = "INSTANCE_IP"
IstiodAddr = "istiod.istio-system.svc:15010"
KitexXdsDomain = "KITEX_XDS_DOMAIN"
Expand All @@ -40,8 +42,12 @@ const (
)

type BootstrapConfig struct {
node *v3core.Node
xdsSvrCfg *XDSServerConfig
// The namespace to make up fqdn.
// Use POD_NAMESPACE default, the meta namespace will override that if set.
configNamespace string
nodeDomain string
node *v3core.Node
xdsSvrCfg *XDSServerConfig
}

type XDSServerConfig struct {
Expand Down Expand Up @@ -88,6 +94,30 @@ func nodeId(podIP, podName, namespace, nodeDomain string) string {
return fmt.Sprintf("sidecar~%s~%s.%s~%s.svc.%s", podIP, podName, namespace, namespace, nodeDomain)
}

// tryExpandFQDN try expand fully qualified domain name.
func (bc *BootstrapConfig) tryExpandFQDN(host string) string {
// The kubernetes services following the <serviceName>.<ns>.svc.<suffix> naming convention
// and that share a suffix with the domain. If it already been expanded, ignore it.
if strings.Contains(host, ".svc.") {
return host
}

var b strings.Builder
b.Grow(len(host) + len(bc.configNamespace) + len(bc.nodeDomain) + 10)
b.WriteString(host)

// the regex for Kubernetes service is [a-z]([-a-z0-9]*[a-z0-9])?, it should not contains `.` in it
// if the host not contains namespace.
if !strings.Contains(host, ".") {
b.WriteString(".")
b.WriteString(bc.configNamespace)
}

b.WriteString(".svc.")
b.WriteString(bc.nodeDomain)
return b.String()
}

// newBootstrapConfig constructs the bootstrapConfig
func newBootstrapConfig(config *XDSServerConfig) (*BootstrapConfig, error) {
// Get info from env
Expand All @@ -114,13 +144,22 @@ func newBootstrapConfig(config *XDSServerConfig) (*BootstrapConfig, error) {
nodeDomain = "cluster.local"
}

metaEnvs := os.Getenv(KitexXdsMetas)

return &BootstrapConfig{
bsConfig := &BootstrapConfig{
nodeDomain: nodeDomain,
configNamespace: namespace,
node: &v3core.Node{
Id: nodeId(podIP, podName, namespace, nodeDomain),
Metadata: parseMetaEnvs(metaEnvs, istioVersion),
Metadata: parseMetaEnvs(os.Getenv(KitexXdsMetas), istioVersion),
},
xdsSvrCfg: config,
}, nil
}

// the priority of NAMESPACE in metadata is higher than POD_NAMESPACE.
// ref: https://github.com/istio/istio/blob/30446a7b88aba4a0fcd5f71bae8d397a874e846f/pilot/pkg/model/context.go#L1024
if field, ok := bsConfig.node.Metadata.Fields[MetaNamespace]; ok {
if val := field.GetStringValue(); val != "" {
bsConfig.configNamespace = val
}
}
return bsConfig, nil
}
45 changes: 45 additions & 0 deletions core/manager/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,51 @@ import (
"google.golang.org/protobuf/types/known/structpb"
)

func TestTryExpandFQDN(t *testing.T) {
testCases := []struct {
desc string
host string
want string
bsc *BootstrapConfig
}{
{
desc: "success",
host: "servicea",
want: "servicea.default.svc.cluster.local",
bsc: &BootstrapConfig{
nodeDomain: "cluster.local",
configNamespace: "default",
},
},
{
desc: "success",
host: "servicea.bookinfo",
want: "servicea.bookinfo.svc.cluster.local",
bsc: &BootstrapConfig{
nodeDomain: "cluster.local",
configNamespace: "default",
},
},
{
desc: "fqdn",
host: "servicea.bookinfo.svc.cluster.local",
want: "servicea.bookinfo.svc.cluster.local",
bsc: &BootstrapConfig{
nodeDomain: "cluster.local",
configNamespace: "default",
},
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
got := tc.bsc.tryExpandFQDN(tc.host)
if got != tc.want {
t.Errorf("tryExpandFQDN() got = %v, want %v", got, tc.want)
}
})
}
}

func TestParseMetaEnvs(t *testing.T) {
testCases := []struct {
desc string
Expand Down
28 changes: 24 additions & 4 deletions core/manager/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,27 @@ func (c *xdsClient) sendRequest(req *discoveryv3.DiscoveryRequest) {
c.reqCh <- req
}

func (c *xdsClient) resolveAddr(host string) string {
// In the worst case, lookupHost is called twice, try to reduce it.
// May exists three kind host:
// 1. fqdn host in Kubernetes, such as example.default.svc.cluster.local, invoke once always.
// 2. short name in Kubernetes, such as example, invoke once when the host exists in cipResolver, and twice when the host does not exist in cipResolver.
// 3. service outside Kubernetes, such as www.example.com, invoke twice always.
// FIXME: format as <serviceName>.<namespace> is not supported.
fqdn := c.config.tryExpandFQDN(host)
cip, ok := c.cipResolver.lookupHost(fqdn)
if ok && len(cip) > 0 {
return cip[0]
}
if fqdn != host {
cip, ok := c.cipResolver.lookupHost(host)
if ok && len(cip) > 0 {
return cip[0]
}
}
return ""
}

// getListenerName returns the listener name in this format: ${clusterIP}_${port}
// lookup the clusterIP using the cipResolver and return the listenerName
func (c *xdsClient) getListenerName(rName string) (string, error) {
Expand All @@ -393,10 +414,9 @@ func (c *xdsClient) getListenerName(rName string) (string, error) {
return "", fmt.Errorf("invalid listener name: %s", rName)
}
addr, port := tmp[0], tmp[1]
cip, ok := c.cipResolver.lookupHost(addr)
if ok && len(cip) > 0 {
clusterIPPort := cip[0] + "_" + port
return clusterIPPort, nil
cip := c.resolveAddr(addr)
if len(cip) > 0 {
return cip + "_" + port, nil
}
return "", fmt.Errorf("failed to convert listener name for %s", rName)
}
Expand Down
53 changes: 53 additions & 0 deletions core/manager/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,56 @@ func TestClearCh(t *testing.T) {
clearRequestCh(ch, 10)
assert.Equal(t, 0, len(ch))
}

func TestResolveAddr(t *testing.T) {
resolver := newNdsResolver()
resolver.updateLookupTable(map[string][]string{
"echoa.default.svc.cluster.local": {"1.1.1.1"},
"echoa.default1.svc.cluster.local": {"1.1.1.1"},
"echob.default.svc.cluster.local": {"1.1.1.1"},
})
cli := &xdsClient{
cipResolver: resolver,
config: &BootstrapConfig{
configNamespace: "default",
nodeDomain: "cluster.local",
},
}
testCases := []struct {
desc string
host string
want string
}{
{
desc: "fqdn",
host: "echoa.default.svc.cluster.local",
want: "1.1.1.1",
},
{
desc: "short name",
host: "echoa",
want: "1.1.1.1",
},
{
desc: "contain namespace",
host: "echoa.default",
want: "1.1.1.1",
},
{
desc: "contain namespace",
host: "echoa.default1",
want: "1.1.1.1",
},
{
desc: "not found",
host: "echoa.default.svc",
want: "",
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
got := cli.resolveAddr(tc.host)
assert.Equal(t, tc.want, got)
})
}
}