github.com/caos/orbos@v1.5.14-0.20221103111702-e6cd0cea7ad4/internal/operator/orbiter/kinds/providers/gce/normalize.go (about)

     1  package gce
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  
     7  	"github.com/caos/orbos/internal/helpers"
     8  
     9  	"google.golang.org/api/googleapi"
    10  
    11  	"github.com/caos/orbos/internal/operator/orbiter"
    12  
    13  	"github.com/caos/orbos/mntr"
    14  
    15  	"google.golang.org/api/compute/v1"
    16  
    17  	"github.com/caos/orbos/internal/operator/orbiter/kinds/loadbalancers/dynamic"
    18  )
    19  
    20  type normalizedLoadbalancer struct {
    21  	forwardingRule *forwardingRule // unique
    22  	targetPool     *targetPool     // unique
    23  	healthcheck    *healthcheck    // unique
    24  	address        *address        // The same externalIP reference appears in multiple normalizedLoadbalancer references
    25  	transport      string
    26  	backendPort    uint16
    27  }
    28  
    29  type StandardLogFunc func(msg string, debug bool) func()
    30  
    31  type forwardingRule struct {
    32  	log StandardLogFunc
    33  	gce *compute.ForwardingRule
    34  }
    35  
    36  type targetPool struct {
    37  	log       func(msg string, debug bool, instances []*instance) func()
    38  	gce       *compute.TargetPool
    39  	destPools []string
    40  }
    41  
    42  type healthcheck struct {
    43  	log           StandardLogFunc
    44  	gce           *compute.HttpHealthCheck
    45  	desired       dynamic.HealthChecks
    46  	proxyProtocol bool
    47  	pools         []string
    48  }
    49  
    50  type firewall struct {
    51  	log StandardLogFunc
    52  	gce *compute.Firewall
    53  }
    54  type address struct {
    55  	log StandardLogFunc
    56  	gce *compute.Address
    57  }
    58  
    59  type normalizedLoadbalancing []*normalizedLoadbalancer
    60  
    61  func (n normalizedLoadbalancing) uniqueAddresses() []*address {
    62  	addresses := make([]*address, 0)
    63  loop:
    64  	for _, lb := range n {
    65  		for _, found := range addresses {
    66  			if lb.address == found {
    67  				continue loop
    68  			}
    69  		}
    70  		addresses = append(addresses, lb.address)
    71  	}
    72  	return addresses
    73  }
    74  
    75  func (n normalizedLoadbalancing) Len() int      { return len(n) }
    76  func (n normalizedLoadbalancing) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
    77  func (n normalizedLoadbalancing) Less(i, j int) bool {
    78  	return n[i].forwardingRule.gce.Description < n[j].forwardingRule.gce.Description
    79  }
    80  
    81  type normalizedDestination struct {
    82  	port  dynamic.Port
    83  	pools []string
    84  	hc    dynamic.HealthChecks
    85  }
    86  
    87  type sortableDestinations []*normalizedDestination
    88  
    89  func (n sortableDestinations) Len() int      { return len(n) }
    90  func (n sortableDestinations) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
    91  func (n sortableDestinations) Less(i, j int) bool {
    92  	return n[i].port < n[j].port || n[i].hc.Protocol < n[i].hc.Protocol || n[i].hc.Path < n[i].hc.Path || n[i].hc.Code < n[i].hc.Code
    93  }
    94  
    95  // normalize returns a normalizedLoadBalancing for each unique destination backendPort and ip combination
    96  // whereas only one random configured healthcheck is relevant
    97  func normalize(ctx *context, spec map[string][]*dynamic.VIP) ([]*normalizedLoadbalancer, []*firewall) {
    98  	var normalized []*normalizedLoadbalancer
    99  	var firewalls []*firewall
   100  
   101  	providerDescription := fmt.Sprintf("orb=%s;provider=%s", ctx.orbID, ctx.providerID)
   102  
   103  	for ipName, ips := range spec {
   104  		for ipIdx, ip := range ips {
   105  			ipID := fmt.Sprintf("%s-%d", ipName, ipIdx)
   106  			normalizedAddress := &address{
   107  				gce: &compute.Address{
   108  					Description: fmt.Sprintf("orb=%s;provider=%s;id=%s", ctx.orbID, ctx.providerID, ipID),
   109  				},
   110  			}
   111  			normalizedAddress.log = func(addr *address, monitor mntr.Monitor, internalID string) func(msg string, debug bool) func() {
   112  				localMonitor := monitor.WithField("internal-id", internalID)
   113  				return func(msg string, debug bool) func() {
   114  					if addr.gce.Name != "" {
   115  						localMonitor = localMonitor.WithField("external-id", addr.gce.Name)
   116  					}
   117  					level := localMonitor.Info
   118  					if debug {
   119  						level = localMonitor.Debug
   120  					}
   121  
   122  					return func() {
   123  						level(msg)
   124  					}
   125  				}
   126  			}(normalizedAddress, ctx.monitor, ipID)
   127  
   128  			addressTransports := make([]string, 0)
   129  			for _, src := range ip.Transport {
   130  				addressTransports = append(addressTransports, src.Name)
   131  				destDescription := fmt.Sprintf("%s;transport=%s", providerDescription, src.Name)
   132  				destMonitor := ctx.monitor.WithFields(map[string]interface{}{
   133  					"transport": src.Name,
   134  				})
   135  				fwr := &compute.ForwardingRule{
   136  					Description:         destDescription,
   137  					LoadBalancingScheme: "EXTERNAL",
   138  					PortRange:           fmt.Sprintf("%d-%d", src.FrontendPort, src.FrontendPort),
   139  				}
   140  
   141  				tp := &compute.TargetPool{
   142  					Description: destDescription,
   143  				}
   144  				hc := &compute.HttpHealthCheck{
   145  					Description: destDescription,
   146  					RequestPath: src.HealthChecks.Path,
   147  				}
   148  
   149  				normalized = append(normalized, &normalizedLoadbalancer{
   150  					backendPort: uint16(src.BackendPort),
   151  					forwardingRule: &forwardingRule{
   152  						log: func(msg string, debug bool) func() {
   153  							localMonitor := destMonitor
   154  							if fwr.Name != "" {
   155  								localMonitor = localMonitor.WithField("id", fwr.Name)
   156  							}
   157  							level := localMonitor.Info
   158  							if debug {
   159  								level = localMonitor.Debug
   160  							}
   161  
   162  							return func() {
   163  								level(msg)
   164  							}
   165  						},
   166  						gce: fwr,
   167  					},
   168  					targetPool: &targetPool{
   169  						log: func(msg string, debug bool, insts []*instance) func() {
   170  							localMonitor := destMonitor
   171  							if len(insts) > 0 {
   172  								localMonitor = localMonitor.WithField("instances", instances(insts).strings(func(i *instance) string { return i.X_ID }))
   173  							}
   174  							if tp.Name != "" {
   175  								localMonitor = localMonitor.WithField("id", tp.Name)
   176  							}
   177  							level := localMonitor.Info
   178  							if debug {
   179  								level = localMonitor.Debug
   180  							}
   181  							return func() {
   182  								level(msg)
   183  							}
   184  						},
   185  						gce:       tp,
   186  						destPools: src.BackendPools,
   187  					},
   188  					healthcheck: &healthcheck{
   189  						log: func(msg string, debug bool) func() {
   190  							localMonitor := destMonitor
   191  							if hc.Name != "" {
   192  								localMonitor = localMonitor.WithField("id", hc.Name)
   193  							}
   194  							level := localMonitor.Info
   195  							if debug {
   196  								level = localMonitor.Debug
   197  							}
   198  
   199  							return func() {
   200  								level(msg)
   201  							}
   202  						},
   203  						gce:           hc,
   204  						desired:       src.HealthChecks,
   205  						pools:         src.BackendPools,
   206  						proxyProtocol: *src.ProxyProtocol,
   207  					},
   208  					address:   normalizedAddress,
   209  					transport: src.Name,
   210  				})
   211  
   212  				firewalls = append(firewalls, toInternalFirewall(&compute.Firewall{
   213  					Network:     ctx.networkURL,
   214  					Description: fmt.Sprintf("External %s", src.Name),
   215  					Allowed: []*compute.FirewallAllowed{{
   216  						IPProtocol: "tcp",
   217  						Ports:      []string{fmt.Sprintf("%d", src.FrontendPort)},
   218  					}},
   219  					SourceRanges: whitelistStrings(src.Whitelist),
   220  					TargetTags:   networkTags(ctx.orbID, ctx.providerID, src.BackendPools...),
   221  				}, destMonitor))
   222  			}
   223  			sort.Strings(addressTransports)
   224  		}
   225  	}
   226  
   227  	sort.Sort(normalizedLoadbalancing(normalized))
   228  
   229  	var hcPort int64 = 6700
   230  	for _, lb := range normalized {
   231  		lb.healthcheck.gce.Port = hcPort
   232  		firewalls = append(firewalls, toInternalFirewall(&compute.Firewall{
   233  			Description: fmt.Sprintf("Healthchecks %s", lb.transport),
   234  			Network:     ctx.networkURL,
   235  			Allowed: []*compute.FirewallAllowed{{
   236  				IPProtocol: "tcp",
   237  				Ports:      []string{fmt.Sprintf("%d", hcPort)},
   238  			}},
   239  			SourceRanges: []string{
   240  				// healthcheck sources, see https://cloud.google.com/load-balancing/docs/health-checks#fw-netlb
   241  				"35.191.0.0/16",
   242  				"209.85.152.0/22",
   243  				"209.85.204.0/22",
   244  			},
   245  			TargetTags: networkTags(ctx.orbID, ctx.providerID, lb.healthcheck.pools...),
   246  		}, ctx.monitor))
   247  		hcPort++
   248  	}
   249  
   250  	return normalized, append(firewalls, toInternalFirewall(&compute.Firewall{
   251  		Network: ctx.networkURL,
   252  		Allowed: []*compute.FirewallAllowed{{
   253  			IPProtocol: "tcp",
   254  			Ports:      []string{"0-65535"},
   255  		}, {
   256  			IPProtocol: "udp",
   257  			Ports:      []string{"0-65535"},
   258  		}, {
   259  			IPProtocol: "icmp",
   260  		}, {
   261  			IPProtocol: "ipip",
   262  		}},
   263  		Description:  "Internal Communication",
   264  		SourceRanges: []string{"10.128.0.0/9"},
   265  		TargetTags:   networkTags(ctx.orbID, ctx.providerID),
   266  	}, ctx.monitor), toInternalFirewall(&compute.Firewall{
   267  		Network: ctx.networkURL,
   268  		Allowed: []*compute.FirewallAllowed{{
   269  			IPProtocol: "tcp",
   270  			Ports:      []string{"22"},
   271  		}},
   272  		Description:  "SSH through IAP",
   273  		SourceRanges: []string{"35.235.240.0/20"},
   274  		TargetTags:   networkTags(ctx.orbID, ctx.providerID),
   275  	}, ctx.monitor))
   276  }
   277  
   278  func toInternalFirewall(fw *compute.Firewall, monitor mntr.Monitor) *firewall {
   279  	return &firewall{
   280  		log: func(msg string, debug bool) func() {
   281  			if fw.Name != "" {
   282  				monitor = monitor.WithField("id", fw.Name)
   283  			}
   284  			level := monitor.Info
   285  			if debug {
   286  				level = monitor.Debug
   287  			}
   288  
   289  			return func() {
   290  				level(msg)
   291  			}
   292  		},
   293  		gce: fw,
   294  	}
   295  }
   296  
   297  func newName() string {
   298  	return "orbos-" + helpers.RandomStringRunes(6, []rune("abcdefghijklmnopqrstuvwxyz0123456789"))
   299  }
   300  
   301  func removeLog(monitor mntr.Monitor, resource, id string, removed bool, debug bool) func() {
   302  	msg := "Removing resource"
   303  	if removed {
   304  		msg = "Resource removed"
   305  	}
   306  	monitor = monitor.WithFields(map[string]interface{}{
   307  		"type": resource,
   308  		"id":   id,
   309  	})
   310  	level := monitor.Info
   311  	if debug {
   312  		level = monitor.Debug
   313  	}
   314  	return func() {
   315  		level(msg)
   316  	}
   317  }
   318  
   319  func removeResourceFunc(monitor mntr.Monitor, resource, id string, call func(...googleapi.CallOption) (*compute.Operation, error)) func() error {
   320  	return func() error {
   321  		if err := operateFunc(
   322  			removeLog(monitor, resource, id, false, true),
   323  			computeOpCall(call),
   324  			nil,
   325  		)(); err != nil {
   326  			googleErr, ok := err.(*googleapi.Error)
   327  			if !ok || googleErr.Code != 404 {
   328  				return err
   329  			}
   330  		}
   331  		removeLog(monitor, resource, id, true, false)()
   332  		return nil
   333  	}
   334  }
   335  
   336  func queryLB(context *context, normalized []*normalizedLoadbalancer) (func() error, error) {
   337  	lb, err := chainInEnsureOrder(
   338  		context, normalized,
   339  		queryHealthchecks,
   340  		queryTargetPools,
   341  		queryAddresses,
   342  		queryForwardingRules,
   343  	)
   344  
   345  	if err != nil {
   346  		return nil, err
   347  	}
   348  
   349  	return func() error {
   350  		for _, fn := range lb {
   351  			if err := fn(); err != nil {
   352  				return err
   353  			}
   354  		}
   355  		return nil
   356  	}, nil
   357  }
   358  
   359  type ensureLBFunc func(*context, []*normalizedLoadbalancer) ([]func() error, []func() error, error)
   360  
   361  type ensureFWFunc func(*context, []*firewall) ([]func() error, []func() error, error)
   362  
   363  func chainInEnsureOrder(ctx *context, lb []*normalizedLoadbalancer, query ...ensureLBFunc) ([]func() error, error) {
   364  	var ensureOperations []func() error
   365  	var removeOperations []func() error
   366  
   367  	for _, fn := range query {
   368  		ensure, remove, err := fn(ctx, lb)
   369  		if err != nil {
   370  			return nil, err
   371  		}
   372  		ensureOperations = append(ensureOperations, helpers.Fanout(ensure))
   373  		removeOperations = append(removeOperations, helpers.Fanout(remove))
   374  	}
   375  
   376  	for i := 0; i < len(removeOperations)/2; i++ {
   377  		j := len(removeOperations) - i - 1
   378  		removeOperations[i], removeOperations[j] = removeOperations[j], removeOperations[i]
   379  	}
   380  
   381  	return append(removeOperations, ensureOperations...), nil
   382  }
   383  
   384  func whitelistStrings(cidrs []*orbiter.CIDR) []string {
   385  	l := len(cidrs)
   386  	wl := make([]string, l, l)
   387  	for idx, cidr := range cidrs {
   388  		wl[idx] = string(*cidr)
   389  	}
   390  	return wl
   391  }