github.com/openshift/installer@v1.4.17/pkg/asset/installconfig/powervs/metadata.go (about) 1 package powervs 2 3 import ( 4 "context" 5 "fmt" 6 "math" 7 "sync" 8 "time" 9 10 "github.com/IBM-Cloud/bluemix-go/crn" 11 "github.com/IBM/vpc-go-sdk/vpcv1" 12 "k8s.io/apimachinery/pkg/util/wait" 13 14 "github.com/openshift/installer/pkg/types" 15 ) 16 17 //go:generate mockgen -source=./metadata.go -destination=./mock/powervsmetadata_generated.go -package=mock 18 19 // MetadataAPI represents functions that eventually call out to the API 20 type MetadataAPI interface { 21 AccountID(ctx context.Context) (string, error) 22 APIKey(ctx context.Context) (string, error) 23 CISInstanceCRN(ctx context.Context) (string, error) 24 DNSInstanceCRN(ctx context.Context) (string, error) 25 } 26 27 // Metadata holds additional metadata for InstallConfig resources that 28 // do not need to be user-supplied (e.g. because it can be retrieved 29 // from external APIs). 30 type Metadata struct { 31 BaseDomain string 32 PublishStrategy types.PublishingStrategy 33 34 accountID string 35 apiKey string 36 cisInstanceCRN string 37 dnsInstanceCRN string 38 client *Client 39 40 mutex sync.Mutex 41 } 42 43 // NewMetadata initializes a new Metadata object. 44 func NewMetadata(config *types.InstallConfig) *Metadata { 45 return &Metadata{BaseDomain: config.BaseDomain, PublishStrategy: config.Publish} 46 } 47 48 // AccountID returns the IBM Cloud account ID associated with the authentication 49 // credentials. 50 func (m *Metadata) AccountID(ctx context.Context) (string, error) { 51 m.mutex.Lock() 52 defer m.mutex.Unlock() 53 54 if m.client == nil { 55 client, err := NewClient() 56 if err != nil { 57 return "", err 58 } 59 60 m.client = client 61 } 62 63 if m.accountID == "" { 64 if m.client.BXCli.User == nil || m.client.BXCli.User.Account == "" { 65 return "", fmt.Errorf("failed to get find account ID: %+v", m.client.BXCli.User) 66 } 67 m.accountID = m.client.BXCli.User.Account 68 } 69 70 return m.accountID, nil 71 } 72 73 // APIKey returns the IBM Cloud account API Key associated with the authentication 74 // credentials. 75 func (m *Metadata) APIKey(ctx context.Context) (string, error) { 76 m.mutex.Lock() 77 defer m.mutex.Unlock() 78 79 if m.client == nil { 80 client, err := NewClient() 81 if err != nil { 82 return "", err 83 } 84 85 m.client = client 86 } 87 88 if m.apiKey == "" { 89 m.apiKey = m.client.GetAPIKey() 90 } 91 92 return m.apiKey, nil 93 } 94 95 // CISInstanceCRN returns the Cloud Internet Services instance CRN that is 96 // managing the DNS zone for the base domain. 97 func (m *Metadata) CISInstanceCRN(ctx context.Context) (string, error) { 98 m.mutex.Lock() 99 defer m.mutex.Unlock() 100 101 var err error 102 if m.client == nil { 103 client, err := NewClient() 104 if err != nil { 105 return "", err 106 } 107 108 m.client = client 109 } 110 111 if m.PublishStrategy == types.ExternalPublishingStrategy && m.cisInstanceCRN == "" { 112 m.cisInstanceCRN, err = m.client.GetInstanceCRNByName(ctx, m.BaseDomain, types.ExternalPublishingStrategy) 113 if err != nil { 114 return "", err 115 } 116 } 117 return m.cisInstanceCRN, nil 118 } 119 120 // SetCISInstanceCRN sets Cloud Internet Services instance CRN to a string value. 121 func (m *Metadata) SetCISInstanceCRN(crn string) { 122 m.cisInstanceCRN = crn 123 } 124 125 // DNSInstanceCRN returns the IBM DNS Service instance CRN that is 126 // managing the DNS zone for the base domain. 127 func (m *Metadata) DNSInstanceCRN(ctx context.Context) (string, error) { 128 m.mutex.Lock() 129 defer m.mutex.Unlock() 130 131 var err error 132 if m.client == nil { 133 client, err := NewClient() 134 if err != nil { 135 return "", err 136 } 137 138 m.client = client 139 } 140 141 if m.PublishStrategy == types.InternalPublishingStrategy && m.dnsInstanceCRN == "" { 142 m.dnsInstanceCRN, err = m.client.GetInstanceCRNByName(ctx, m.BaseDomain, types.InternalPublishingStrategy) 143 if err != nil { 144 return "", err 145 } 146 } 147 148 return m.dnsInstanceCRN, nil 149 } 150 151 // SetDNSInstanceCRN sets IBM DNS Service instance CRN to a string value. 152 func (m *Metadata) SetDNSInstanceCRN(crn string) { 153 m.dnsInstanceCRN = crn 154 } 155 156 // GetExistingVPCGateway checks if the VPC is a Permitted Network for the DNS Zone 157 func (m *Metadata) GetExistingVPCGateway(ctx context.Context, vpcName string, vpcSubnet string) (string, bool, error) { 158 if vpcName == "" || vpcSubnet == "" { 159 return "", false, nil 160 } 161 162 vpc, err := m.client.GetVPCByName(ctx, vpcName) 163 if err != nil { 164 return "", false, fmt.Errorf("failed to get VPC: %w", err) 165 } 166 167 vpcCRN, err := crn.Parse(*vpc.CRN) 168 if err != nil { 169 return "", false, fmt.Errorf("failed to parse VPC CRN: %w", err) 170 } 171 172 subnet, err := m.client.GetSubnetByName(ctx, vpcSubnet, vpcCRN.Region) 173 if err != nil { 174 return "", false, fmt.Errorf("failed to get subnet: %w", err) 175 } 176 // Check if subnet has an attached public gateway. If it does, we're done. 177 if subnet.PublicGateway != nil { 178 return *subnet.PublicGateway.Name, true, nil 179 } 180 181 // Check if a gateway exists in the VPN that isn't attached 182 gw, err := m.client.GetPublicGatewayByVPC(ctx, vpcName) 183 if err != nil { 184 return "", false, fmt.Errorf("failed to get find gw: %w", err) 185 } 186 // Found an unattached gateway 187 if gw != nil { 188 return *gw.Name, false, nil 189 } 190 return "", false, nil 191 } 192 193 // IsVPCPermittedNetwork checks if the VPC is a Permitted Network for the DNS Zone 194 func (m *Metadata) IsVPCPermittedNetwork(ctx context.Context, vpcName string, baseDomain string) (bool, error) { 195 // An empty pre-existing VPC Name signifies a new VPC will be created (not pre-existing), so it won't be permitted 196 if vpcName == "" { 197 return false, nil 198 } 199 200 // Collect DNSInstance details if not already collected 201 if m.dnsInstanceCRN == "" { 202 _, err := m.DNSInstanceCRN(ctx) 203 if err != nil { 204 return false, fmt.Errorf("cannot collect DNS permitted networks without DNS Instance: %w", err) 205 } 206 } 207 208 if m.client == nil { 209 client, err := NewClient() 210 if err != nil { 211 return false, err 212 } 213 214 m.client = client 215 } 216 217 // Get CIS zone ID by name 218 zoneID, err := m.client.GetDNSZoneIDByName(context.TODO(), baseDomain, types.InternalPublishingStrategy) 219 if err != nil { 220 return false, fmt.Errorf("failed to get DNS zone ID: %w", err) 221 } 222 223 dnsCRN, err := crn.Parse(m.dnsInstanceCRN) 224 if err != nil { 225 return false, fmt.Errorf("failed to parse DNSInstanceCRN: %w", err) 226 } 227 228 networks, err := m.client.GetDNSInstancePermittedNetworks(ctx, dnsCRN.ServiceInstance, zoneID) 229 if err != nil { 230 return false, err 231 } 232 if len(networks) < 1 { 233 return false, nil 234 } 235 236 vpc, err := m.client.GetVPCByName(ctx, vpcName) 237 if err != nil { 238 return false, err 239 } 240 for _, network := range networks { 241 if network == *vpc.CRN { 242 return true, nil 243 } 244 } 245 246 return false, nil 247 } 248 249 // EnsureVPCIsPermittedNetwork checks if a VPC is permitted to the DNS zone and adds it if it is not. 250 func (m *Metadata) EnsureVPCIsPermittedNetwork(ctx context.Context, vpcName string) error { 251 dnsCRN, err := crn.Parse(m.dnsInstanceCRN) 252 if err != nil { 253 return fmt.Errorf("failed to parse DNSInstanceCRN: %w", err) 254 } 255 256 isVPCPermittedNetwork, err := m.IsVPCPermittedNetwork(ctx, vpcName, m.BaseDomain) 257 if err != nil { 258 return fmt.Errorf("failed to determine if VPC is permitted network: %w", err) 259 } 260 261 if !isVPCPermittedNetwork { 262 vpc, err := m.client.GetVPCByName(ctx, vpcName) 263 if err != nil { 264 return fmt.Errorf("failed to find VPC by name: %w", err) 265 } 266 267 zoneID, err := m.client.GetDNSZoneIDByName(ctx, m.BaseDomain, types.InternalPublishingStrategy) 268 if err != nil { 269 return fmt.Errorf("failed to get DNS zone ID: %w", err) 270 } 271 err = m.client.AddVPCToPermittedNetworks(ctx, *vpc.CRN, dnsCRN.ServiceInstance, zoneID) 272 if err != nil { 273 return fmt.Errorf("failed to add permitted network: %w", err) 274 } 275 } 276 return nil 277 } 278 279 // GetSubnetID gets the ID of a VPC subnet by name and region. 280 func (m *Metadata) GetSubnetID(ctx context.Context, subnetName string, vpcRegion string) (string, error) { 281 subnet, err := m.client.GetSubnetByName(ctx, subnetName, vpcRegion) 282 if err != nil { 283 return "", err 284 } 285 return *subnet.ID, err 286 } 287 288 // GetVPCSubnets gets a list of subnets in a VPC. 289 func (m *Metadata) GetVPCSubnets(ctx context.Context, vpcName string) ([]vpcv1.Subnet, error) { 290 vpc, err := m.client.GetVPCByName(ctx, vpcName) 291 if err != nil { 292 return nil, err 293 } 294 subnets, err := m.client.GetVPCSubnets(ctx, *vpc.ID) 295 if err != nil { 296 return nil, fmt.Errorf("failed to get VPC subnets: %w", err) 297 } 298 return subnets, err 299 } 300 301 // GetDNSServerIP gets the IP of a custom resolver for DNS use. 302 func (m *Metadata) GetDNSServerIP(ctx context.Context, vpcName string) (string, error) { 303 vpc, err := m.client.GetVPCByName(ctx, vpcName) 304 if err != nil { 305 return "", err 306 } 307 308 dnsCRN, err := crn.Parse(m.dnsInstanceCRN) 309 if err != nil { 310 return "", fmt.Errorf("failed to parse DNSInstanceCRN: %w", err) 311 } 312 dnsServerIP, err := m.client.GetDNSCustomResolverIP(ctx, dnsCRN.ServiceInstance, *vpc.ID) 313 if err != nil { 314 // There is no custom resolver, try to create one. 315 customResolverName := fmt.Sprintf("%s-custom-resolver", vpcName) 316 customResolver, err := m.client.CreateDNSCustomResolver(ctx, customResolverName, dnsCRN.ServiceInstance, *vpc.ID) 317 if err != nil { 318 return "", err 319 } 320 // Wait for the custom resolver to be enabled. 321 backoff := wait.Backoff{ 322 Duration: 15 * time.Second, 323 Factor: 1.1, 324 Cap: leftInContext(ctx), 325 Steps: math.MaxInt32} 326 327 customResolverID := *customResolver.ID 328 var lastErr error 329 err = wait.ExponentialBackoffWithContext(ctx, backoff, func(context.Context) (bool, error) { 330 customResolver, lastErr = m.client.EnableDNSCustomResolver(ctx, dnsCRN.ServiceInstance, customResolverID) 331 if lastErr == nil { 332 return true, nil 333 } 334 return false, nil 335 }) 336 if err != nil { 337 if lastErr != nil { 338 err = lastErr 339 } 340 return "", fmt.Errorf("failed to enable custom resolver %s: %w", *customResolver.ID, err) 341 } 342 dnsServerIP = *customResolver.Locations[0].DnsServerIp 343 } 344 return dnsServerIP, nil 345 } 346 347 // CreateDNSRecord creates a CNAME record for the specified hostname and destination hostname. 348 func (m *Metadata) CreateDNSRecord(ctx context.Context, hostname string, destHostname string) error { 349 instanceCRN, err := m.client.GetInstanceCRNByName(ctx, m.BaseDomain, m.PublishStrategy) 350 if err != nil { 351 return fmt.Errorf("failed to get InstanceCRN (%s) by name: %w", m.PublishStrategy, err) 352 } 353 354 backoff := wait.Backoff{ 355 Duration: 15 * time.Second, 356 Factor: 1.1, 357 Cap: leftInContext(ctx), 358 Steps: math.MaxInt32} 359 360 var lastErr error 361 err = wait.ExponentialBackoffWithContext(ctx, backoff, func(context.Context) (bool, error) { 362 lastErr = m.client.CreateDNSRecord(ctx, m.PublishStrategy, instanceCRN, m.BaseDomain, hostname, destHostname) 363 if lastErr == nil { 364 return true, nil 365 } 366 return false, nil 367 }) 368 369 if err != nil { 370 if lastErr != nil { 371 err = lastErr 372 } 373 return fmt.Errorf("failed to create a DNS CNAME record (%s, %s): %w", 374 hostname, 375 destHostname, 376 err) 377 } 378 return err 379 } 380 381 // ListSecurityGroupRules lists the rules created in the specified VPC. 382 func (m *Metadata) ListSecurityGroupRules(ctx context.Context, vpcID string) (*vpcv1.SecurityGroupRuleCollection, error) { 383 return m.client.ListSecurityGroupRules(ctx, vpcID) 384 } 385 386 // SetVPCServiceURLForRegion sets the URL for the VPC based on the specified region. 387 func (m *Metadata) SetVPCServiceURLForRegion(ctx context.Context, vpcRegion string) error { 388 return m.client.SetVPCServiceURLForRegion(ctx, vpcRegion) 389 } 390 391 // AddSecurityGroupRule adds a security group rule to the specified VPC. 392 func (m *Metadata) AddSecurityGroupRule(ctx context.Context, rule *vpcv1.SecurityGroupRulePrototype, vpcID string) error { 393 backoff := wait.Backoff{ 394 Duration: 15 * time.Second, 395 Factor: 1.1, 396 Cap: leftInContext(ctx), 397 Steps: math.MaxInt32} 398 399 var lastErr error 400 err := wait.ExponentialBackoffWithContext(ctx, backoff, func(context.Context) (bool, error) { 401 lastErr = m.client.AddSecurityGroupRule(ctx, vpcID, rule) 402 if lastErr == nil { 403 return true, nil 404 } 405 return false, nil 406 }) 407 408 if err != nil { 409 if lastErr != nil { 410 err = lastErr 411 } 412 return fmt.Errorf("failed to add security group rule: %w", err) 413 } 414 return err 415 } 416 417 func leftInContext(ctx context.Context) time.Duration { 418 deadline, ok := ctx.Deadline() 419 if !ok { 420 return math.MaxInt64 421 } 422 423 return time.Until(deadline) 424 }