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  }