github.com/operator-framework/operator-lifecycle-manager@v0.30.0/pkg/controller/registry/grpc/source.go (about)

     1  package grpc
     2  
     3  import (
     4  	"context"
     5  	"net"
     6  	"net/url"
     7  	"os"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/operator-framework/operator-registry/pkg/client"
    12  	"github.com/sirupsen/logrus"
    13  	"golang.org/x/net/http/httpproxy"
    14  	"golang.org/x/net/proxy"
    15  	"google.golang.org/grpc"
    16  	"google.golang.org/grpc/connectivity"
    17  	"google.golang.org/grpc/credentials/insecure"
    18  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    19  
    20  	"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry"
    21  )
    22  
    23  type SourceMeta struct {
    24  	Address         string
    25  	LastConnect     metav1.Time
    26  	ConnectionState connectivity.State
    27  }
    28  
    29  type SourceState struct {
    30  	Key   registry.CatalogKey
    31  	State connectivity.State
    32  }
    33  
    34  type SourceConn struct {
    35  	SourceMeta
    36  	Conn   *grpc.ClientConn
    37  	cancel context.CancelFunc
    38  }
    39  
    40  type SourceStore struct {
    41  	sync.Once
    42  	sources      map[registry.CatalogKey]SourceConn
    43  	sourcesLock  sync.RWMutex
    44  	syncFn       func(SourceState)
    45  	logger       *logrus.Logger
    46  	notify       chan SourceState
    47  	timeout      time.Duration
    48  	readyTimeout time.Duration
    49  }
    50  
    51  func NewSourceStore(logger *logrus.Logger, timeout, readyTimeout time.Duration, sync func(SourceState)) *SourceStore {
    52  	return &SourceStore{
    53  		sources:      make(map[registry.CatalogKey]SourceConn),
    54  		notify:       make(chan SourceState),
    55  		syncFn:       sync,
    56  		logger:       logger,
    57  		timeout:      timeout,
    58  		readyTimeout: readyTimeout,
    59  	}
    60  }
    61  
    62  func (s *SourceStore) Start(ctx context.Context) {
    63  	s.logger.Debug("starting source manager")
    64  	go func() {
    65  		s.Do(func() {
    66  			for {
    67  				select {
    68  				case <-ctx.Done():
    69  					s.logger.Debug("closing source manager")
    70  					return
    71  				case e := <-s.notify:
    72  					s.logger.Debugf("Got source event: %#v", e)
    73  					s.syncFn(e)
    74  				}
    75  			}
    76  		})
    77  	}()
    78  }
    79  
    80  func (s *SourceStore) GetMeta(key registry.CatalogKey) *SourceMeta {
    81  	s.sourcesLock.RLock()
    82  	source, ok := s.sources[key]
    83  	s.sourcesLock.RUnlock()
    84  	if !ok {
    85  		return nil
    86  	}
    87  
    88  	return &source.SourceMeta
    89  }
    90  
    91  func (s *SourceStore) Exists(key registry.CatalogKey) bool {
    92  	s.sourcesLock.RLock()
    93  	_, ok := s.sources[key]
    94  	s.sourcesLock.RUnlock()
    95  	return ok
    96  }
    97  
    98  func (s *SourceStore) Get(key registry.CatalogKey) *SourceConn {
    99  	s.sourcesLock.RLock()
   100  	source, ok := s.sources[key]
   101  	s.sourcesLock.RUnlock()
   102  	if !ok {
   103  		return nil
   104  	}
   105  	return &source
   106  }
   107  
   108  func grpcProxyURL(addr string) (*url.URL, error) {
   109  	// Handle ip addresses
   110  	host, _, err := net.SplitHostPort(addr)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	url, err := url.Parse(host)
   116  	if err != nil {
   117  		return nil, err
   118  	}
   119  
   120  	// Hardcode fields required for proxy resolution
   121  	url.Host = addr
   122  	url.Scheme = "http"
   123  
   124  	// Override HTTPS_PROXY and HTTP_PROXY with GRPC_PROXY
   125  	proxyConfig := &httpproxy.Config{
   126  		HTTPProxy:  getGRPCProxyEnv(),
   127  		HTTPSProxy: getGRPCProxyEnv(),
   128  		NoProxy:    getEnvAny("NO_PROXY", "no_proxy"),
   129  		CGI:        os.Getenv("REQUEST_METHOD") != "",
   130  	}
   131  
   132  	// Check if a proxy should be used based on environment variables
   133  	return proxyConfig.ProxyFunc()(url)
   134  }
   135  
   136  func getGRPCProxyEnv() string {
   137  	return getEnvAny("GRPC_PROXY", "grpc_proxy")
   138  }
   139  
   140  func getEnvAny(names ...string) string {
   141  	for _, n := range names {
   142  		if val := os.Getenv(n); val != "" {
   143  			return val
   144  		}
   145  	}
   146  	return ""
   147  }
   148  
   149  func grpcConnection(address string) (*grpc.ClientConn, error) {
   150  	dialOptions := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
   151  	proxyURL, err := grpcProxyURL(address)
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  
   156  	if proxyURL != nil {
   157  		dialOptions = append(dialOptions, grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
   158  			dialer, err := proxy.FromURL(proxyURL, &net.Dialer{})
   159  			if err != nil {
   160  				return nil, err
   161  			}
   162  			return dialer.Dial("tcp", addr)
   163  		}))
   164  	}
   165  
   166  	return grpc.Dial(address, dialOptions...)
   167  }
   168  
   169  func (s *SourceStore) Add(key registry.CatalogKey, address string) (*SourceConn, error) {
   170  	_ = s.Remove(key)
   171  
   172  	conn, err := grpcConnection(address)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	ctx, cancel := context.WithCancel(context.Background())
   178  	source := SourceConn{
   179  		SourceMeta: SourceMeta{
   180  			Address:         address,
   181  			LastConnect:     metav1.Now(),
   182  			ConnectionState: connectivity.Idle,
   183  		},
   184  		Conn:   conn,
   185  		cancel: cancel,
   186  	}
   187  
   188  	s.sourcesLock.Lock()
   189  	s.sources[key] = source
   190  	s.sourcesLock.Unlock()
   191  
   192  	go s.watch(ctx, key, source)
   193  
   194  	return &source, nil
   195  }
   196  
   197  func (s *SourceStore) stateTimeout(state connectivity.State) time.Duration {
   198  	if state == connectivity.Ready {
   199  		return s.readyTimeout
   200  	}
   201  	return s.timeout
   202  }
   203  
   204  func (s *SourceStore) watch(ctx context.Context, key registry.CatalogKey, source SourceConn) {
   205  	state := source.ConnectionState
   206  	for {
   207  		select {
   208  		case <-ctx.Done():
   209  			return
   210  		default:
   211  			func() {
   212  				timer, cancel := context.WithTimeout(ctx, s.stateTimeout(state))
   213  				defer cancel()
   214  				if source.Conn.WaitForStateChange(timer, state) {
   215  					newState := source.Conn.GetState()
   216  					state = newState
   217  
   218  					// update connection state
   219  					src := s.Get(key)
   220  					if src == nil {
   221  						// source was removed, cleanup this goroutine
   222  						return
   223  					}
   224  
   225  					src.LastConnect = metav1.Now()
   226  					src.ConnectionState = newState
   227  					s.sourcesLock.Lock()
   228  					s.sources[key] = *src
   229  					s.sourcesLock.Unlock()
   230  
   231  					// Always try to reconnect. If the connection is already connected, this is a no-op.
   232  					//
   233  					// This function is non-blocking. Therefore, when it returns we'll still return IDLE
   234  					// as the state (we'll see further state changes in subsequent iterations of the loop).
   235  					source.Conn.Connect()
   236  
   237  					// notify subscriber
   238  					s.notify <- SourceState{Key: key, State: newState}
   239  				}
   240  			}()
   241  		}
   242  	}
   243  }
   244  
   245  func (s *SourceStore) Remove(key registry.CatalogKey) error {
   246  	s.sourcesLock.RLock()
   247  	source, ok := s.sources[key]
   248  	s.sourcesLock.RUnlock()
   249  
   250  	// no source to close
   251  	if !ok {
   252  		return nil
   253  	}
   254  
   255  	s.sourcesLock.Lock()
   256  	delete(s.sources, key)
   257  	s.sourcesLock.Unlock()
   258  
   259  	// clean up watcher
   260  	source.cancel()
   261  
   262  	return source.Conn.Close()
   263  }
   264  
   265  func (s *SourceStore) AsClients(namespaces ...string) map[registry.CatalogKey]registry.ClientInterface {
   266  	refs := map[registry.CatalogKey]registry.ClientInterface{}
   267  	s.sourcesLock.RLock()
   268  	defer s.sourcesLock.RUnlock()
   269  	for key, source := range s.sources {
   270  		if source.LastConnect.IsZero() {
   271  			continue
   272  		}
   273  		for _, namespace := range namespaces {
   274  			if key.Namespace == namespace {
   275  				refs[key] = registry.NewClientFromConn(source.Conn)
   276  			}
   277  		}
   278  	}
   279  
   280  	// TODO : remove unhealthy
   281  	return refs
   282  }
   283  
   284  func (s *SourceStore) ClientsForNamespaces(namespaces ...string) map[registry.CatalogKey]client.Interface {
   285  	refs := map[registry.CatalogKey]client.Interface{}
   286  	s.sourcesLock.RLock()
   287  	defer s.sourcesLock.RUnlock()
   288  	for key, source := range s.sources {
   289  		if source.LastConnect.IsZero() {
   290  			continue
   291  		}
   292  		for _, namespace := range namespaces {
   293  			if key.Namespace == namespace {
   294  				refs[key] = client.NewClientFromConn(source.Conn)
   295  			}
   296  		}
   297  	}
   298  
   299  	// TODO : remove unhealthy
   300  	return refs
   301  }