diff --git a/api/v1beta2/types.go b/api/v1beta2/types.go index 3d81846bc..d315628b7 100644 --- a/api/v1beta2/types.go +++ b/api/v1beta2/types.go @@ -252,6 +252,8 @@ var ( ResourceTypeDHCPServer = ResourceType("dhcpServer") // ResourceTypeLoadBalancer VPC loadBalancer resource. ResourceTypeLoadBalancer = ResourceType("loadBalancer") + // ResourceTypeLoadBalancerPool is a Load Balancer Pool resource. + ResourceTypeLoadBalancerPool = ResourceType("loadBalancerPool") // ResourceTypeTransitGateway is transit gateway resource. ResourceTypeTransitGateway = ResourceType("transitGateway") // ResourceTypeVPC is Power VS network resource. diff --git a/cloud/scope/vpc_cluster.go b/cloud/scope/vpc_cluster.go index 7e0658283..98197a115 100644 --- a/cloud/scope/vpc_cluster.go +++ b/cloud/scope/vpc_cluster.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "net/http" "reflect" "github.com/go-logr/logr" @@ -54,6 +55,11 @@ const ( // vpcSubnetIPVersion4 defines the IP v4 string used for VPC Subnet generation. vpcSubnetIPVersion4 = "ipv4" + + // privateLBSuffix is used to tag a default Load Balancer name as private. + privateLBSuffix = "private" + // publicLBSuffix is used to tag a default Load Balancer name as public. + publicLBSuffix = "public" ) // VPCClusterScopeParams defines the input parameters used to create a new VPCClusterScope. @@ -221,6 +227,93 @@ func (s *VPCClusterScope) CheckTagExists(tagName string) (bool, error) { return exists != nil, nil } +// GetAPIServerPort will return the API Server's port. +func (s *VPCClusterScope) GetAPIServerPort() int32 { + // TODO(cjschaef): Add logic to handle cases not default. + return infrav1beta2.DefaultAPIServerPort +} + +// GetControlPlaneSubnetIDs returns all of the Control Plane subnet Id's. +func (s *VPCClusterScope) GetControlPlaneSubnetIDs() ([]string, error) { + subnets := make([]string, 0) + // Retrieve the subnet Id's from Status. + if s.NetworkStatus() != nil && s.NetworkStatus().ControlPlaneSubnets != nil { + for _, subnet := range s.NetworkStatus().ControlPlaneSubnets { + subnets = append(subnets, subnet.ID) + } + // NOTE(cjschaef): We assume all Subnets are in Status at this point, we could perhaps reconcile Status with any defined in Spec (preventing duplicates) to be safe. + return subnets, nil + } + + // NOTE(cjschaef): If Status was not set or ControlPlaneSubnets was empty, the Control Plane subnet ID's could be retrieved from Spec. However, for now consider this an error, since Subnet reconciliation should have run prior and no tracked Control Plane subnets would be a major issue. + return subnets, fmt.Errorf("error no control plane subnets available in status") +} + +// GetLoadBalancerHostName will return the hostname of the cluster's public Load Balancer, assuming only one public Load Balancer was provided. Or, the hostname of the single private Load Balancer (assuming the cluster has no public access and only one private Load Balancer was provided). +// This function has a very hard assumption that all Load Balancers have been reconciled within Status (and not just some). +// NOTE(cjschaef): A webhook validation check could help ensure this. +func (s *VPCClusterScope) GetLoadBalancerHostName() (*string, error) { + // If no Status or Load Balancer Status is populated, assume the Load Balancer's are not ready (have not been reconciled), so no hostname will be available. + if s.NetworkStatus() == nil || s.NetworkStatus().LoadBalancers == nil || len(s.NetworkStatus().LoadBalancers) == 0 { + return nil, nil + } + + // If there is only one Load Balancer in Status, return the hostname. + // This heavily assumes all Load Balancers have been reconciled and are in Status. + if len(s.NetworkStatus().LoadBalancers) == 1 { + for _, lb := range s.NetworkStatus().LoadBalancers { + // There should only be one key-value pair in the map. + return lb.Hostname, nil + } + } + + // If no Load Balancer's were defined, return an error, as a Load Balancer must be defined (no default Load Balancer is supported currently). + if len(s.NetworkSpec().LoadBalancers) == 0 { + return nil, fmt.Errorf("error no load balancers defined for cluster") + } + + // Otherwise, if more than one Load Balancer was provided, attempt to use the public Load Balancer's hostname. + // TODO(cjschaef): A webhook valiation check could guarantee only one public Load Balancer gets defined, as this will simply return the first public Load Balancer (currently only support one public Load Balancer being defined). + for _, loadBalancer := range s.NetworkSpec().LoadBalancers { + // Check if the Load Balancer is not public (by default it is, when Public is not defined). + // This heavily assumes there is only be one public Load Balancer. + if loadBalancer.Public != nil && !*loadBalancer.Public { + continue + } + + // If an ID was provided in Spec, try to find that within Status. + if loadBalancer.ID != nil { + if lb, ok := s.NetworkStatus().LoadBalancers[*loadBalancer.ID]; ok { + return lb.Hostname, nil + } + return nil, fmt.Errorf("error defined load balancer not found in status: %s", *loadBalancer.ID) + } + + // If the defined Load Balancer name was not supplied (empty), assume one was created using the default service name format (with type suffix). + // This heavily assumes only two Load Balancers maximum can be supplied (one public and one private) at this time. + name := loadBalancer.Name + if name == "" { + lbSuffix := publicLBSuffix + if loadBalancer.Public != nil && !*loadBalancer.Public { + lbSuffix = privateLBSuffix + } + name = fmt.Sprintf("%s-%s", *s.GetServiceName(infrav1beta2.ResourceTypeLoadBalancer), lbSuffix) + } + + // Retrieve the Load Balancer hostname from API. + lbDetails, err := s.VPCClient.GetLoadBalancerByName(name) + if err != nil { + return nil, fmt.Errorf("error retrieving load balancer hostname for %s: %w", name, err) + } else if lbDetails == nil { + return nil, fmt.Errorf("error retrieving load balancer hostname, %s load balancer not found", name) + } + return lbDetails.Hostname, nil + } + + // If no public Load Balancer or more than one private Load Balancer was found in Spec, expect that a proper Load Balancer was not specified (a default public Load Balancer isn't supported), or cannot be determined. + return nil, fmt.Errorf("error no valid load balancer found to retrieve hostname") +} + // GetNetworkResourceGroupID returns the Resource Group ID for the Network Resources if it is present. Otherwise, it defaults to the cluster's Resource Group ID. func (s *VPCClusterScope) GetNetworkResourceGroupID() (string, error) { // Check if the ID is available from Status first. @@ -300,6 +393,25 @@ func (s *VPCClusterScope) GetResourceGroupID() (string, error) { return *resourceGroup.ID, nil } +// GetSecurityGroupID returns the ID of a security group, provided the name. +// This will first check Status for the Security Group (by name), but as the Security Group may not be tracked by CAPI, a lookup of the Security Group by name is made via the VPC API. +func (s *VPCClusterScope) GetSecurityGroupID(name string) (*string, error) { + // Check Status first. + if id := s.getSecurityGroupIDFromStatus(name); id != nil { + return id, nil + } + + // Otherwise, if no Status, or not found, attempt to look it up via VPC API. + securityGroup, err := s.VPCClient.GetSecurityGroupByName(name) + if err != nil { + return nil, err + } + if securityGroup == nil { + return nil, nil + } + return securityGroup.ID, nil +} + func (s *VPCClusterScope) getSecurityGroupIDFromStatus(name string) *string { if s.NetworkStatus() != nil && s.NetworkStatus().SecurityGroups != nil { if sg, ok := s.NetworkStatus().SecurityGroups[name]; ok { @@ -328,6 +440,12 @@ func (s *VPCClusterScope) GetServiceName(resourceType infrav1beta2.ResourceType) case infrav1beta2.ResourceTypePublicGateway: // Generate a generic public gateway name based off the cluster name, which can be extedned as necessary (for Zone). return ptr.To(fmt.Sprintf("%s-pgateway", s.IBMVPCCluster.Name)) + case infrav1beta2.ResourceTypeLoadBalancer: + // Generate a generic load balancer name based off the cluster name, which can be extended as necessary (for public vs private). + return ptr.To(fmt.Sprintf("%s-lb", s.IBMVPCCluster.Name)) + case infrav1beta2.ResourceTypeLoadBalancerPool: + // Generate a generic load balancer pool name based off the cluster name, which can be extended as necessary (for LB). + return ptr.To(fmt.Sprintf("%s-lbpool", s.IBMVPCCluster.Name)) default: s.V(3).Info("unsupported resource type", "resourceType", resourceType) } @@ -390,6 +508,25 @@ func (s *VPCClusterScope) GetVPCID() (*string, error) { return nil, nil } +// setLoadBalancerStatus sets the status for a Load Balancer. +func (s *VPCClusterScope) setLoadBalancerStatus(loadBalancer *infrav1beta2.VPCLoadBalancerStatus) { + s.V(3).Info("Setting status for Load Balancer", "loadBalancer", loadBalancer) + if s.NetworkStatus() == nil { + s.IBMVPCCluster.Status.Network = &infrav1beta2.VPCNetworkStatus{} + } + if s.NetworkStatus().LoadBalancers == nil { + s.IBMVPCCluster.Status.Network.LoadBalancers = make(map[string]*infrav1beta2.VPCLoadBalancerStatus) + } + if lb, ok := s.NetworkStatus().LoadBalancers[*loadBalancer.ID]; ok { + // ID should not change, update remaining fields. + lb.State = loadBalancer.State + // Hostname likely should not change either, but may not be available initially, so may need to be set later. + lb.Hostname = loadBalancer.Hostname + } else { + s.IBMVPCCluster.Status.Network.LoadBalancers[*loadBalancer.ID] = loadBalancer + } +} + // SetResourceStatus sets the status for the provided ResourceType. func (s *VPCClusterScope) SetResourceStatus(resourceType infrav1beta2.ResourceType, resource *infrav1beta2.ResourceStatus) { //nolint:gocyclo // Ignore attempts to set status without resource. @@ -1646,3 +1783,387 @@ func (s *VPCClusterScope) createSecurityGroupRuleRemote(remote infrav1beta2.VPCS return remotePrototype, nil } + +// ReconcileLoadBalancers reconciles Load Balancers. +func (s *VPCClusterScope) ReconcileLoadBalancers() (bool, error) { + // TODO(cjschaef): Determine if we want to use default LB configuration or require at least one is defined in Cluster spec. + // TODO(cjschaef): Remove in favor of webhook validation. Perhaps to limit the number of LB's to one public and one private maximum. + if len(s.NetworkSpec().LoadBalancers) == 0 { + // We currently don't support any default LB configuration, they must be specified within the Cluster spec. + return false, fmt.Errorf("error no load balancers specified for cluster") + } else if len(s.NetworkSpec().LoadBalancers) > 2 { + // We currently only support up to two LB configurations. This can be limiting in management, but due to complexities of design and support, this is the easiest method currently. + return false, fmt.Errorf("error maximum of two load balancers can be defined for a cluster, %d supplied", len(s.NetworkSpec().LoadBalancers)) + } + + // Attempt to reconcile each Load Balancer before requeing, if necessary. + requeue := false + for _, loadBalancer := range s.IBMVPCCluster.Spec.Network.LoadBalancers { + // Attempt to retrieve the Load Balancer by Name or ID. + lbStatus, err := s.getLoadBalancer(loadBalancer) + if err != nil { + return false, fmt.Errorf("error retrieving load balancer: %w", err) + } + + // If the Load Balancer was found, update Status and move on. + if lbStatus != nil { + s.setLoadBalancerStatus(lbStatus) + // If the Load Balancer status isn't ready, flag for requeue and continue to next Load Balancer. + if isReady := s.isLoadBalancerReady(lbStatus.State); !isReady { + requeue = true + } + continue + } + + // Otherwise, create the Load Balancer. + err = s.createLoadBalancer(loadBalancer) + if err != nil { + return false, fmt.Errorf("error creating load balancer: %w", err) + } + // Assume a new Load Balancer will not be ready immediately, due to the complexity and time it takes. + requeue = true + } + return requeue, nil +} + +// isLoadBalancerReady checks the state of a Load Balancer. +// If state is active, true is returned, in all other cases, it returns false. +// NOTE(cjschaef): May wish to extend this function to check all Load Balancer details (pools, listeners, etc.) as part of a Load Balancer being ready. +func (s *VPCClusterScope) isLoadBalancerReady(status infrav1beta2.VPCLoadBalancerState) bool { + switch status { + case infrav1beta2.VPCLoadBalancerStateActive: + s.V(5).Info("load balancer is in active state") + return true + case infrav1beta2.VPCLoadBalancerStateCreatePending: + s.V(5).Info("load balancer is in create pending state") + default: + s.V(5).Info("load balancer is in unexpected state", "loadBalancerStatus", status) + } + return false +} + +// getLoadBalancer attempts to retrieve the Load Balancer, otherwise returns nil if it doesn't exist. +func (s *VPCClusterScope) getLoadBalancer(lb infrav1beta2.VPCLoadBalancerSpec) (*infrav1beta2.VPCLoadBalancerStatus, error) { + var loadBalancer *vpcv1.LoadBalancer + var err error + if lb.ID != nil { + var detailedResponse *core.DetailedResponse + getLBOptions := &vpcv1.GetLoadBalancerOptions{ + ID: lb.ID, + } + loadBalancer, detailedResponse, err = s.VPCClient.GetLoadBalancer(getLBOptions) + if (detailedResponse != nil && detailedResponse.StatusCode == http.StatusNotFound) || loadBalancer == nil { + return nil, fmt.Errorf("error failed to retrieve load balancer with id %s", *lb.ID) + } + } else { + name := lb.Name + if name == "" { + // As LB's within Spec are limited to two maximum, we expect at most one public and one private. Append 'pubic' or 'private' to the name, depending on the LB definition. + lbSuffix := publicLBSuffix + if lb.Public != nil && !*lb.Public { + lbSuffix = privateLBSuffix + } + name = fmt.Sprintf("%s-%s", *s.GetServiceName(infrav1beta2.ResourceTypeLoadBalancer), lbSuffix) + } + loadBalancer, err = s.VPCClient.GetLoadBalancerByName(name) + } + if err != nil { + return nil, fmt.Errorf("error attempting to retrieve load balancer: %w", err) + } + if loadBalancer == nil { + return nil, nil + } + return &infrav1beta2.VPCLoadBalancerStatus{ + ID: loadBalancer.ID, + State: infrav1beta2.VPCLoadBalancerState(*loadBalancer.ProvisioningStatus), + Hostname: loadBalancer.Hostname, + }, nil +} + +// createLoadBalancer creates a Load Balancer. +func (s *VPCClusterScope) createLoadBalancer(loadBalancer infrav1beta2.VPCLoadBalancerSpec) error { + options := &vpcv1.CreateLoadBalancerOptions{} + resourceGroupID, err := s.GetResourceGroupID() + if err != nil { + return err + } + if resourceGroupID == "" { + return fmt.Errorf("error getting resource group id for resource group %v, id is empty", s.IBMVPCCluster.Spec.ResourceGroup) + } + + isPublic := true + // Load Balancer is private if defined that way (defaults to Public) + if loadBalancer.Public != nil && !*loadBalancer.Public { + isPublic = false + } + + options.SetIsPublic(isPublic) + + name := loadBalancer.Name + // If the provided Load Balancer does not have a name defined, generate a default one, and append the type (public versus private) to distinguish, rather than rely on the API to generate a random name. + // Currently, there is a hard limit of 2 maximum LB's, although they could both be private (or public), so additional validation is required to handle those cases. + if name == "" { + // As LB's within Spec are limited to two maximum, we expect at most one public and one private. Append 'pubic' or 'private' to the name, depending on the LB definition. + lbSuffix := publicLBSuffix + if !isPublic { + lbSuffix = privateLBSuffix + } + name = fmt.Sprintf("%s-%s", *s.GetServiceName(infrav1beta2.ResourceTypeLoadBalancer), lbSuffix) + } + options.SetName(name) + + options.SetResourceGroup(&vpcv1.ResourceGroupIdentity{ + ID: &resourceGroupID, + }) + + // Build the load balancer's subnets, requiring subnet ID's. + subnetIDs, err := s.getLoadBalancerSubnetIDs(loadBalancer) + if err != nil { + return fmt.Errorf("error collecting load balancer subnets: %w", err) + } + for _, subnetID := range subnetIDs { + subnet := &vpcv1.SubnetIdentityByID{ + ID: ptr.To(subnetID), + } + s.V(3).Info("adding subnet to load balancer", "loadBalancerName", loadBalancer.Name, "subnetID", subnetID) + options.Subnets = append(options.Subnets, subnet) + } + + // Build the load balancer's security groups, requiring security group ID's. + securityGroupIDs, err := s.getLoadBalancerSecurityGroupIDs(loadBalancer) + if err != nil { + return fmt.Errorf("error collecting load balancer security groups: %w", err) + } + for _, securityGroupID := range securityGroupIDs { + sg := &vpcv1.SecurityGroupIdentityByID{ + ID: ptr.To(securityGroupID), + } + s.V(3).Info("adding security group to load balancer", "loadBalancerName", loadBalancer.Name, "securityGroupID", securityGroupID) + options.SecurityGroups = append(options.SecurityGroups, sg) + } + + // Build the load balancer's backend pools. + backendPools := make([]vpcv1.LoadBalancerPoolPrototype, 0) + // If BackendPools is populated, use those. Otherwise, use default. + // TODO(cjschaef): Determine if a default Pool should be auto generated, or allow "empty" pools for LB's. + if loadBalancer.BackendPools != nil { + for _, pool := range loadBalancer.BackendPools { + backendPool := s.buildLoadBalancerBackendPool(pool) + + s.V(3).Info("added pool to load balancer", "loadBalancerName", loadBalancer.Name, "backendPoolName", pool.Name) + backendPools = append(backendPools, backendPool) + } + } else { + s.V(3).Info("using default backend pools for load balancer", "loadBalancerName", loadBalancer.Name) + backendPools = append(backendPools, s.getDefaultLoadBalancerBackendPools()...) + } + options.SetPools(backendPools) + + // Build the load balancer's listeners. + listeners := make([]vpcv1.LoadBalancerListenerPrototypeLoadBalancerContext, 0) + // If AdditionalListeners is populated, use those. Otherwise, use default. + // TODO(cjschaef): Determine if a default Listener should be auto generated or allow "empty" listeners for LB's. + if loadBalancer.AdditionalListeners != nil { + for _, additionalListener := range loadBalancer.AdditionalListeners { + listener := s.buildLoadBalancerListener(additionalListener) + + s.V(3).Info("addd listener to load balancer", "loadBalancerName", loadBalancer.Name, "listenerPort", listener.Port) + listeners = append(listeners, listener) + } + } else { + s.V(3).Info("using default listeners for load balancer", "loadBalancerName", loadBalancer.Name) + listeners = append(listeners, s.getDefaultLoadBalancerListeners(loadBalancer.BackendPools == nil)...) + } + options.SetListeners(listeners) + + // Create the load balancer. + s.V(5).Info("creating new load balancer", "loadBalancerOptions", options) + loadBalancerDetails, _, err := s.VPCClient.CreateLoadBalancer(options) + if err != nil { + return fmt.Errorf("error creating load balancer: %w", err) + } + + // Initially populate the Load Balancer's status. + s.setLoadBalancerStatus(&infrav1beta2.VPCLoadBalancerStatus{ + ID: loadBalancerDetails.ID, + ControllerCreated: ptr.To(true), + Hostname: loadBalancerDetails.Hostname, + State: infrav1beta2.VPCLoadBalancerState(*loadBalancerDetails.ProvisioningStatus), + }) + + // NOTE: This tagging is only attempted once. We may wish to refactor in case this single attempt fails. + if err = s.TagResource(s.IBMVPCCluster.Name, *loadBalancerDetails.CRN); err != nil { + return fmt.Errorf("error tagging load balancer: %w", err) + } + + return nil +} + +// getLoadBalancerSubnetIDs builds the set of subnet ID's for a load balancer, or defaults to the Control Plane subnet ID's if no subnets were provided. This will attempt to transform subnet names into their respective ID's. +func (s *VPCClusterScope) getLoadBalancerSubnetIDs(loadBalancer infrav1beta2.VPCLoadBalancerSpec) ([]string, error) { + subnetIDs := make([]string, 0) + // If Subnets were provided for the load balancer, find ID's, if necessary, and use them. + // Otherwise, default to trying to use the Control Plane subnets. + if loadBalancer.Subnets != nil { + for _, subnet := range loadBalancer.Subnets { + if subnet.ID != nil { + // Check that the subnet exists. + subnetOptions := &vpcv1.GetSubnetOptions{ + ID: subnet.ID, + } + subnetDetails, _, err := s.VPCClient.GetSubnet(subnetOptions) + if err != nil { + return nil, fmt.Errorf("error looking up load balancer subnet by id %s: %w", *subnet.ID, err) + } else if subnetDetails == nil { + return nil, fmt.Errorf("error load balancer subnet no found: %s", *subnet.ID) + } + subnetIDs = append(subnetIDs, *subnet.ID) + continue + } + if subnet.Name != nil { + subnetID, err := s.GetSubnetID(*subnet.Name) + if err != nil { + return nil, fmt.Errorf("error looking up load balancer subnet by name %s: %w", *subnet.Name, err) + } else if subnetID == nil { + return nil, fmt.Errorf("error load balancer subnet not found: %s", *subnet.Name) + } + subnetIDs = append(subnetIDs, *subnetID) + } else { + // TODO(cjschaef: This could potentially be covered by webhook validation. + return nil, fmt.Errorf("error parsing load balancer subnet, no id or name provided: %s", loadBalancer.Name) + } + } + } else { + var err error + subnetIDs, err = s.GetControlPlaneSubnetIDs() + if err != nil { + return nil, fmt.Errorf("error collecting subnet IDs for load balancer creation: %w", err) + } + } + return subnetIDs, nil +} + +// getLoadBalancerSecurityGroupIDs will collect the ID's of the desired Security Groups for a Load Balancer. +func (s *VPCClusterScope) getLoadBalancerSecurityGroupIDs(loadBalancer infrav1beta2.VPCLoadBalancerSpec) ([]string, error) { + securityGroupIDs := make([]string, 0) + // If SecurityGroups were provided for the load balancer, find ID's, if necessary, and use them. + if loadBalancer.SecurityGroups != nil { + for _, securityGroup := range loadBalancer.SecurityGroups { + if securityGroup.ID != nil { + // Check that the Security Group exists. + sgOptions := &vpcv1.GetSecurityGroupOptions{ + ID: securityGroup.ID, + } + sgDetails, _, err := s.VPCClient.GetSecurityGroup(sgOptions) + if err != nil { + return nil, fmt.Errorf("error looking up load balancer security group by id %s: %w", *securityGroup.ID, err) + } else if sgDetails == nil { + return nil, fmt.Errorf("error load balancer security group not found: %s", *securityGroup.ID) + } + securityGroupIDs = append(securityGroupIDs, *securityGroup.ID) + continue + } + if securityGroup.Name != nil { + // A Security Group may not be managed or tracked by CAPI (an existing Security Group), so do not expect it must exist in Status. + securityGroupID, err := s.GetSecurityGroupID(*securityGroup.Name) + if err != nil { + return nil, fmt.Errorf("error looking up load balancer security group by name %s: %w", *securityGroup.Name, err) + } else if securityGroupID == nil { + return nil, fmt.Errorf("error load balancer security group not found: %s", *securityGroup.Name) + } + securityGroupIDs = append(securityGroupIDs, *securityGroupID) + } else { + return nil, fmt.Errorf("error parsing load balancer security group, no id or name provided: %s", loadBalancer.Name) + } + } + } + return securityGroupIDs, nil +} + +// buildLoadBalancerBackendPool will build a Load Balancer Pool based on the provided spec. +func (s *VPCClusterScope) buildLoadBalancerBackendPool(pool infrav1beta2.VPCLoadBalancerBackendPoolSpec) vpcv1.LoadBalancerPoolPrototype { + monitor := &vpcv1.LoadBalancerPoolHealthMonitorPrototype{ + Delay: ptr.To(pool.HealthMonitor.Delay), + MaxRetries: ptr.To(pool.HealthMonitor.Retries), + Timeout: ptr.To(pool.HealthMonitor.Timeout), + Type: ptr.To(string(pool.HealthMonitor.Type)), + } + if pool.HealthMonitor.Port != nil { + monitor.Port = pool.HealthMonitor.Port + } + if pool.HealthMonitor.URLPath != nil { + monitor.URLPath = pool.HealthMonitor.URLPath + } + backendPool := vpcv1.LoadBalancerPoolPrototype{ + Algorithm: ptr.To(string(pool.Algorithm)), + HealthMonitor: monitor, + Protocol: ptr.To(string(pool.Protocol)), + } + // Only apply a name if one was provided (otherwise rely on generated name from VPC service). + if pool.Name != nil { + backendPool.Name = pool.Name + } + + return backendPool +} + +// getDefaultBalancerBackendPools returns a list of default Load Balancer Backend Pools for a Load Balancer. +func (s *VPCClusterScope) getDefaultLoadBalancerBackendPools() []vpcv1.LoadBalancerPoolPrototype { + defaultPools := make([]vpcv1.LoadBalancerPoolPrototype, 0) + + // For now, only one default pool is expected. + defaultPool := infrav1beta2.VPCLoadBalancerBackendPoolSpec{ + Algorithm: infrav1beta2.VPCLoadBalancerBackendPoolAlgorithmRoundRobin, + HealthMonitor: infrav1beta2.VPCLoadBalancerHealthMonitorSpec{ + Delay: 5, + Retries: 2, + Timeout: 2, + Type: infrav1beta2.VPCLoadBalancerBackendPoolHealthMonitorTypeTCP, + }, + // Use default backend pool service name. + Name: s.GetServiceName(infrav1beta2.ResourceTypeLoadBalancerPool), + Protocol: infrav1beta2.VPCLoadBalancerBackendPoolProtocolTCP, + } + + defaultPools = append(defaultPools, s.buildLoadBalancerBackendPool(defaultPool)) + return defaultPools +} + +// buildLoadBalancerListener will create a Load Balancer Listener based on the provided spec. +func (s *VPCClusterScope) buildLoadBalancerListener(additionalListener infrav1beta2.AdditionalListenerSpec) vpcv1.LoadBalancerListenerPrototypeLoadBalancerContext { + listener := vpcv1.LoadBalancerListenerPrototypeLoadBalancerContext{ + Port: ptr.To(additionalListener.Port), + // Default protocol to TCP. + Protocol: ptr.To(string(infrav1beta2.VPCLoadBalancerListenerProtocolTCP)), + } + // Override protocol if it was defined. + if additionalListener.Protocol != nil { + listener.Protocol = ptr.To(string(*additionalListener.Protocol)) + } + // Set the Pool name for the listener if it was defined. + if additionalListener.DefaultPoolName != nil { + listener.DefaultPool = &vpcv1.LoadBalancerPoolIdentityByName{ + Name: additionalListener.DefaultPoolName, + } + } + + return listener +} + +// getDefaultLoadBalancerListeners returns a list of default Load Balancer Listeners for a Load Balancer. +func (s *VPCClusterScope) getDefaultLoadBalancerListeners(defaultBackendPool bool) []vpcv1.LoadBalancerListenerPrototypeLoadBalancerContext { + defaultListeners := make([]vpcv1.LoadBalancerListenerPrototypeLoadBalancerContext, 0) + + // For now only one default listener is expected. + defaultListener := infrav1beta2.AdditionalListenerSpec{ + Port: int64(s.GetAPIServerPort()), + Protocol: ptr.To(infrav1beta2.VPCLoadBalancerListenerProtocolTCP), + } + + if defaultBackendPool { + defaultListener.DefaultPoolName = s.GetServiceName(infrav1beta2.ResourceTypeLoadBalancerPool) + } + + defaultListeners = append(defaultListeners, s.buildLoadBalancerListener(defaultListener)) + return defaultListeners +} diff --git a/controllers/ibmvpccluster_controller.go b/controllers/ibmvpccluster_controller.go index 0b4f73033..2f6b37558 100644 --- a/controllers/ibmvpccluster_controller.go +++ b/controllers/ibmvpccluster_controller.go @@ -287,10 +287,33 @@ func (r *IBMVPCClusterReconciler) reconcileCluster(clusterScope *scope.VPCCluste clusterScope.Info("Reconciliation of Security Groups complete") conditions.MarkTrue(clusterScope.IBMVPCCluster, infrav1beta2.VPCSecurityGroupReadyCondition) - // TODO(cjschaef): add remaining resource reconciliation. + // Reconcile the cluster's Load Balancers + clusterScope.Info("Reconciling Load Balancers") + if requeue, err := clusterScope.ReconcileLoadBalancers(); err != nil { + clusterScope.Error(err, "failed to reconcile Load Balancers") + conditions.MarkFalse(clusterScope.IBMVPCCluster, infrav1beta2.LoadBalancerReadyCondition, infrav1beta2.LoadBalancerReconciliationFailedReason, capiv1beta1.ConditionSeverityError, "%s", err.Error()) + return reconcile.Result{}, err + } else if requeue { + clusterScope.Info("Load Balancers creation is pending, requeueing") + return reconcile.Result{RequeueAfter: 15 * time.Second}, nil + } + clusterScope.Info("Reconciliation of Load Balancers complete") + conditions.MarkTrue(clusterScope.IBMVPCCluster, infrav1beta2.LoadBalancerReadyCondition) + + // Collect cluster's Load Balancer hostname for spec. + hostName, err := clusterScope.GetLoadBalancerHostName() + if err != nil { + return reconcile.Result{}, fmt.Errorf("error retrieving load balancer hostname: %w", err) + } else if hostName == nil || *hostName == "" { + clusterScope.Info("No Load Balancer hostname found, requeueing") + return reconcile.Result{RequeueAfter: 15 * time.Second}, nil + } // Mark cluster as ready. + clusterScope.IBMVPCCluster.Spec.ControlPlaneEndpoint.Host = *hostName + clusterScope.IBMVPCCluster.Spec.ControlPlaneEndpoint.Port = clusterScope.GetAPIServerPort() clusterScope.IBMVPCCluster.Status.Ready = true + clusterScope.Info("cluster infrastructure is now ready for cluster", "clusterName", clusterScope.IBMVPCCluster.Name) return ctrl.Result{}, nil }