1 package ipranger
2
3 import (
4 "context"
5 "errors"
6 "fmt"
7 "net/netip"
8 "strconv"
9 "strings"
10 "sync"
11
12 "go4.org/netipx"
13 "google.golang.org/api/compute/v1"
14 )
15
16 const Netmask = 21
17 const rangePerSubnet = 30
18 const rangePerRequest = 2
19 const allIPs = "10.0.0.0/8"
20 const gcpIPs = "10.128.0.0/9"
21
22 type IPRanger struct {
23 mu sync.Mutex
24 subnetSvc computeSubnetSvc
25
26 stores map[string]*rangeStore
27 }
28
29 type Subnet struct {
30 Network string
31 Name string
32 Ref string
33 region string
34 aliasRangeQuota int
35 ipRange string
36 }
37
38 type rangeStore struct {
39
40
41
42
43
44
45 subnets map[string]map[string]Subnet
46 ipset *netipx.IPSet
47 }
48
49 func New(ctx context.Context) (*IPRanger, error) {
50 subnetSvc, err := newGCPSubnetService(ctx)
51 if err != nil {
52 return nil, err
53 }
54 return &IPRanger{
55 subnetSvc: subnetSvc,
56 }, nil
57 }
58
59
60
61
62
63
64 func (r *IPRanger) FindOrCreateSubnet(project, region string) (Subnet, error) {
65 r.mu.Lock()
66 defer r.mu.Unlock()
67
68
69 err := r.InitState(project)
70 if err != nil {
71 return Subnet{}, fmt.Errorf("failed to initialize state. err: %v", err)
72 }
73
74
75 ok, s := r.FindAvailableSubnet(project, region)
76 if ok {
77 return s, nil
78 }
79
80
81 s, err = r.CreateNewSubnet(project, region)
82 if err != nil {
83 return Subnet{}, fmt.Errorf("failed to create subnet. region: %v err: %v", region, err)
84 }
85 return s, nil
86 }
87
88
89 func (r *IPRanger) CreateNewSubnet(project, region string) (Subnet, error) {
90
91 prefix, _, ok := r.stores[project].ipset.RemoveFreePrefix(Netmask)
92 if !ok {
93 return Subnet{}, errors.New("no ip ranges remaining")
94 }
95 name := r.nextSubnetName(project)
96 newSubnet, err := r.subnetSvc.Create(project, prefix.String(), region, name)
97 if err != nil {
98 return Subnet{}, err
99 }
100
101
102
103
104
105 return fromComputeSubnetwork(newSubnet), nil
106 }
107
108
109
110 func (r *IPRanger) FindAvailableSubnet(project, region string) (ok bool, s Subnet) {
111
112 subnets := r.stores[project].subnets[region]
113 if subnets == nil {
114 return false, Subnet{}
115 }
116 for _, s := range subnets {
117 if s.aliasRangeQuota >= rangePerRequest {
118 return true, s
119 }
120 }
121
122 return false, Subnet{}
123 }
124
125
126
127 func (r *IPRanger) nextSubnetName(project string) string {
128 var next int64
129 for _, v := range r.stores[project].subnets {
130 for name := range v {
131 snum := strings.TrimPrefix(name, "subnet")
132 if snum == name {
133
134
135 continue
136 }
137
138
139 num, _ := strconv.ParseInt(snum, 10, 64)
140 if num >= next {
141
142 next = num + 1
143 }
144 }
145 }
146 return fmt.Sprintf("subnet%d", next)
147 }
148
149 func fromComputeSubnetwork(csn *compute.Subnetwork) Subnet {
150 return Subnet{
151 Network: csn.Network,
152 Name: csn.Name,
153 Ref: csn.SelfLink,
154 region: fromRegionURI(csn.Region),
155 aliasRangeQuota: rangePerSubnet - len(csn.SecondaryIpRanges),
156 ipRange: csn.IpCidrRange,
157 }
158 }
159
160
161
162 func buildRangeStore(subnets computeSubnetAggregatedList) (*rangeStore, error) {
163 store := &rangeStore{
164 subnets: map[string]map[string]Subnet{},
165 }
166
167 var existingIPs netipx.IPSetBuilder
168 for regionRegion, v := range subnets {
169
170 rr := strings.Split(regionRegion, "/")
171 if len(rr) != 2 {
172 return nil, fmt.Errorf("invalid region key %v. expected 'regions/$region'", regionRegion)
173 }
174 region := rr[1]
175 store.subnets[region] = map[string]Subnet{}
176
177 for _, sn := range v.Subnetworks {
178 subnet := fromComputeSubnetwork(sn)
179
180
181 subnetPrefix, err := netip.ParsePrefix(subnet.ipRange)
182 if err != nil {
183 return nil, fmt.Errorf("failed to parse ip and prefix from subnet range '%s', err: %v", subnet.ipRange, err)
184 }
185 existingIPs.AddPrefix(subnetPrefix)
186 for _, aliasRange := range sn.SecondaryIpRanges {
187 aliasPrefix, err := netip.ParsePrefix(aliasRange.IpCidrRange)
188 if err != nil {
189 return nil, fmt.Errorf("failed to parse ip and prefix from secondary range '%s', err: %v", aliasRange.IpCidrRange, err)
190 }
191 existingIPs.AddPrefix(aliasPrefix)
192 }
193
194 store.subnets[region][subnet.Name] = subnet
195 }
196 }
197 existingSet, err := existingIPs.IPSet()
198 if err != nil {
199 return nil, fmt.Errorf("failed to build IPSet, err: %v", err)
200 }
201
202 var availableIPs netipx.IPSetBuilder
203
204 availableIPs.AddPrefix(netip.MustParsePrefix(allIPs))
205
206 availableIPs.RemoveSet(existingSet)
207
208 availableIPs.RemovePrefix(netip.MustParsePrefix(gcpIPs))
209
210 availableSet, err := availableIPs.IPSet()
211 if err != nil {
212 return nil, err
213 }
214 store.ipset = availableSet
215
216 return store, nil
217 }
218
219
220
221 func (r *IPRanger) InitState(project string) error {
222 if r.stores == nil {
223 r.stores = make(map[string]*rangeStore)
224 }
225 subnets, err := r.subnetSvc.List(project)
226 if err != nil {
227 return fmt.Errorf("failed to list subnets. err: %v", err)
228 }
229
230 store, err := buildRangeStore(subnets)
231 if err != nil {
232 return err
233 }
234 r.stores[project] = store
235
236 return nil
237 }
238
239 func fromRegionURI(uri string) string {
240 split := strings.Split(uri, "/")
241 return split[len(split)-1]
242 }
243
244 func toRegionURI(project, region string) string {
245 return fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/regions/%s", project, region)
246 }
247
View as plain text