1/*
2 *
3 * Copyright 2020 gRPC authors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *     http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 *
17 */
18
19package xdsclient
20
21import (
22	"bytes"
23	"encoding/json"
24	"fmt"
25	"sync"
26	"time"
27
28	"google.golang.org/grpc/xds/internal/xdsclient/bootstrap"
29)
30
31const defaultWatchExpiryTimeout = 15 * time.Second
32
33// This is the Client returned by New(). It contains one client implementation,
34// and maintains the refcount.
35var singletonClient = &clientRefCounted{}
36
37// To override in tests.
38var bootstrapNewConfig = bootstrap.NewConfig
39
40// clientRefCounted is ref-counted, and to be shared by the xds resolver and
41// balancer implementations, across multiple ClientConns and Servers.
42type clientRefCounted struct {
43	*clientImpl
44
45	// This mu protects all the fields, including the embedded clientImpl above.
46	mu       sync.Mutex
47	refCount int
48}
49
50// New returns a new xdsClient configured by the bootstrap file specified in env
51// variable GRPC_XDS_BOOTSTRAP or GRPC_XDS_BOOTSTRAP_CONFIG.
52//
53// The returned xdsClient is a singleton. This function creates the xds client
54// if it doesn't already exist.
55//
56// Note that the first invocation of New() or NewWithConfig() sets the client
57// singleton. The following calls will return the singleton xds client without
58// checking or using the config.
59func New() (XDSClient, error) {
60	// This cannot just return newRefCounted(), because in error cases, the
61	// returned nil is a typed nil (*clientRefCounted), which may cause nil
62	// checks fail.
63	c, err := newRefCounted()
64	if err != nil {
65		return nil, err
66	}
67	return c, nil
68}
69
70func newRefCounted() (*clientRefCounted, error) {
71	singletonClient.mu.Lock()
72	defer singletonClient.mu.Unlock()
73	// If the client implementation was created, increment ref count and return
74	// the client.
75	if singletonClient.clientImpl != nil {
76		singletonClient.refCount++
77		return singletonClient, nil
78	}
79
80	// Create the new client implementation.
81	config, err := bootstrapNewConfig()
82	if err != nil {
83		return nil, fmt.Errorf("xds: failed to read bootstrap file: %v", err)
84	}
85	c, err := newWithConfig(config, defaultWatchExpiryTimeout)
86	if err != nil {
87		return nil, err
88	}
89
90	singletonClient.clientImpl = c
91	singletonClient.refCount++
92	return singletonClient, nil
93}
94
95// NewWithConfig returns a new xdsClient configured by the given config.
96//
97// The returned xdsClient is a singleton. This function creates the xds client
98// if it doesn't already exist.
99//
100// Note that the first invocation of New() or NewWithConfig() sets the client
101// singleton. The following calls will return the singleton xds client without
102// checking or using the config.
103//
104// This function is internal only, for c2p resolver and testing to use. DO NOT
105// use this elsewhere. Use New() instead.
106func NewWithConfig(config *bootstrap.Config) (XDSClient, error) {
107	singletonClient.mu.Lock()
108	defer singletonClient.mu.Unlock()
109	// If the client implementation was created, increment ref count and return
110	// the client.
111	if singletonClient.clientImpl != nil {
112		singletonClient.refCount++
113		return singletonClient, nil
114	}
115
116	// Create the new client implementation.
117	c, err := newWithConfig(config, defaultWatchExpiryTimeout)
118	if err != nil {
119		return nil, err
120	}
121
122	singletonClient.clientImpl = c
123	singletonClient.refCount++
124	return singletonClient, nil
125}
126
127// Close closes the client. It does ref count of the xds client implementation,
128// and closes the gRPC connection to the management server when ref count
129// reaches 0.
130func (c *clientRefCounted) Close() {
131	c.mu.Lock()
132	defer c.mu.Unlock()
133	c.refCount--
134	if c.refCount == 0 {
135		c.clientImpl.Close()
136		// Set clientImpl back to nil. So if New() is called after this, a new
137		// implementation will be created.
138		c.clientImpl = nil
139	}
140}
141
142// NewWithConfigForTesting is exported for testing only.
143//
144// Note that this function doesn't set the singleton, so that the testing states
145// don't leak.
146func NewWithConfigForTesting(config *bootstrap.Config, watchExpiryTimeout time.Duration) (XDSClient, error) {
147	cl, err := newWithConfig(config, watchExpiryTimeout)
148	if err != nil {
149		return nil, err
150	}
151	return &clientRefCounted{clientImpl: cl, refCount: 1}, nil
152}
153
154// NewClientWithBootstrapContents returns an xds client for this config,
155// separate from the global singleton.  This should be used for testing
156// purposes only.
157func NewClientWithBootstrapContents(contents []byte) (XDSClient, error) {
158	// Normalize the contents
159	buf := bytes.Buffer{}
160	err := json.Indent(&buf, contents, "", "")
161	if err != nil {
162		return nil, fmt.Errorf("xds: error normalizing JSON: %v", err)
163	}
164	contents = bytes.TrimSpace(buf.Bytes())
165
166	clientsMu.Lock()
167	defer clientsMu.Unlock()
168	if c := clients[string(contents)]; c != nil {
169		c.mu.Lock()
170		// Since we don't remove the *Client from the map when it is closed, we
171		// need to recreate the impl if the ref count dropped to zero.
172		if c.refCount > 0 {
173			c.refCount++
174			c.mu.Unlock()
175			return c, nil
176		}
177		c.mu.Unlock()
178	}
179
180	bcfg, err := bootstrap.NewConfigFromContents(contents)
181	if err != nil {
182		return nil, fmt.Errorf("xds: error with bootstrap config: %v", err)
183	}
184
185	cImpl, err := newWithConfig(bcfg, defaultWatchExpiryTimeout)
186	if err != nil {
187		return nil, err
188	}
189
190	c := &clientRefCounted{clientImpl: cImpl, refCount: 1}
191	clients[string(contents)] = c
192	return c, nil
193}
194
195var (
196	clients   = map[string]*clientRefCounted{}
197	clientsMu sync.Mutex
198)
199