Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions pkg/asset/installconfig/ibmcloud/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1528,6 +1528,75 @@ func (c *Client) getKeyServiceAPI(crn ibmcrn.CRN) (*kpclient.Client, error) {
return kpclient.New(clientConfig, kpclient.DefaultTransport())
}

// ListFloatingIPs lists all floating IPs in the region.
func (c *Client) ListFloatingIPs(ctx context.Context, region string) ([]vpcv1.FloatingIP, error) {
localContext, cancel := context.WithTimeout(ctx, 1*time.Minute)
defer cancel()

if err := c.SetVPCServiceURLForRegion(localContext, region); err != nil {
return nil, fmt.Errorf("failed to set vpc api service url: %w", err)
}

pager, err := c.vpcAPI.NewFloatingIpsPager(c.vpcAPI.NewListFloatingIpsOptions())
if err != nil {
return nil, fmt.Errorf("failed to create floating ips pager: %w", err)
}
return pager.GetAllWithContext(localContext)
}

// ListSecurityGroups lists security groups in the region.
// If vpcID is non-empty, results are scoped to that VPC.
func (c *Client) ListSecurityGroups(ctx context.Context, region string, vpcID string) ([]vpcv1.SecurityGroup, error) {
localContext, cancel := context.WithTimeout(ctx, 1*time.Minute)
defer cancel()

if err := c.SetVPCServiceURLForRegion(localContext, region); err != nil {
return nil, fmt.Errorf("failed to set vpc api service url: %w", err)
}

options := c.vpcAPI.NewListSecurityGroupsOptions()
if vpcID != "" {
options.SetVPCID(vpcID)
}
pager, err := c.vpcAPI.NewSecurityGroupsPager(options)
if err != nil {
return nil, fmt.Errorf("failed to create security groups pager: %w", err)
}
return pager.GetAllWithContext(localContext)
}

// ListLoadBalancers lists all load balancers in the region.
func (c *Client) ListLoadBalancers(ctx context.Context, region string) ([]vpcv1.LoadBalancer, error) {
localContext, cancel := context.WithTimeout(ctx, 1*time.Minute)
defer cancel()

if err := c.SetVPCServiceURLForRegion(localContext, region); err != nil {
return nil, fmt.Errorf("failed to set vpc api service url: %w", err)
}

pager, err := c.vpcAPI.NewLoadBalancersPager(c.vpcAPI.NewListLoadBalancersOptions())
if err != nil {
return nil, fmt.Errorf("failed to create load balancers pager: %w", err)
}
return pager.GetAllWithContext(localContext)
}

// ListInstances lists all instances in the region.
func (c *Client) ListInstances(ctx context.Context, region string) ([]vpcv1.Instance, error) {
localContext, cancel := context.WithTimeout(ctx, 1*time.Minute)
defer cancel()

if err := c.SetVPCServiceURLForRegion(localContext, region); err != nil {
return nil, fmt.Errorf("failed to set vpc api service url: %w", err)
}

pager, err := c.vpcAPI.NewInstancesPager(c.vpcAPI.NewListInstancesOptions())
if err != nil {
return nil, fmt.Errorf("failed to create instances pager: %w", err)
}
return pager.GetAllWithContext(localContext)
}

// ListCOSBuckets lists Buckets in the specified COS Instance.
func (c *Client) ListCOSBuckets(ctx context.Context, cosInstanceID string, region string) (*ibms3.ListBucketsOutput, error) {
localContext, cancel := context.WithTimeout(ctx, 1*time.Minute)
Expand Down
7 changes: 7 additions & 0 deletions pkg/asset/quota/ibmcloud/OWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# See the OWNERS docs: https://git.k8s.io/community/contributors/guide/owners.md
# This file just uses aliases defined in OWNERS_ALIASES.

approvers:
- ibmcloud-approvers
reviewers:
- ibmcloud-reviewers
85 changes: 85 additions & 0 deletions pkg/asset/quota/ibmcloud/ibmcloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package ibmcloud

import (
"context"
"fmt"

ibmcloudic "github.com/openshift/installer/pkg/asset/installconfig/ibmcloud"
"github.com/openshift/installer/pkg/quota"
"github.com/openshift/installer/pkg/types"
)

// defaultLimits are IBM Cloud VPC default resource limits.
// Actual limits may vary by account. The check warns rather than
// blocks when it cannot determine the actual limit.
var defaultLimits = map[string]int64{
"is/floating-ip": 20,
"is/security-group": 25,
"is/load-balancer": 50,
"is/instance": 200,
"is/vpc": 10,
}

// Load fetches current IBM Cloud VPC resource usage for quota comparison.
// The security group count is scoped to the target VPC when deploying
// into an existing VPC, since the limit is per-VPC.
func Load(ctx context.Context, client *ibmcloudic.Client, config *types.InstallConfig) ([]quota.Quota, error) {
region := config.Platform.IBMCloud.Region

// Resolve VPC ID for scoped security group counting.
var vpcID string
if config.Platform.IBMCloud.VPCName != "" {
vpc, err := client.GetVPCByName(ctx, config.Platform.IBMCloud.VPCName)
if err != nil {
return nil, fmt.Errorf("looking up VPC %q: %w", config.Platform.IBMCloud.VPCName, err)
}
if vpc != nil && vpc.ID != nil {
vpcID = *vpc.ID
}
}

type counter struct {
name string
fn func() (int, error)
}

counters := []counter{
{"is/floating-ip", func() (int, error) {
fips, err := client.ListFloatingIPs(ctx, region)
return len(fips), err
}},
{"is/security-group", func() (int, error) {
sgs, err := client.ListSecurityGroups(ctx, region, vpcID)
return len(sgs), err
}},
{"is/load-balancer", func() (int, error) {
lbs, err := client.ListLoadBalancers(ctx, region)
return len(lbs), err
}},
{"is/instance", func() (int, error) {
instances, err := client.ListInstances(ctx, region)
return len(instances), err
}},
{"is/vpc", func() (int, error) {
vpcs, err := client.GetVPCs(ctx, region)
return len(vpcs), err
}},
}

quotas := make([]quota.Quota, 0, len(counters))
for _, c := range counters {
count, err := c.fn()
if err != nil {
return nil, fmt.Errorf("counting %s: %w", c.name, err)
}
quotas = append(quotas, quota.Quota{
Service: "is",
Name: c.name,
Region: region,
InUse: int64(count),
Limit: defaultLimits[c.name],
})
}

return quotas, nil
}
21 changes: 20 additions & 1 deletion pkg/asset/quota/quota.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@ import (
"github.com/openshift/installer/pkg/asset"
"github.com/openshift/installer/pkg/asset/installconfig"
awsconfig "github.com/openshift/installer/pkg/asset/installconfig/aws"
ibmcloudic "github.com/openshift/installer/pkg/asset/installconfig/ibmcloud"
openstackvalidation "github.com/openshift/installer/pkg/asset/installconfig/openstack/validation"
configpowervs "github.com/openshift/installer/pkg/asset/installconfig/powervs"
"github.com/openshift/installer/pkg/asset/machines"
"github.com/openshift/installer/pkg/asset/quota/aws"
"github.com/openshift/installer/pkg/asset/quota/gcp"
ibmcloudquota "github.com/openshift/installer/pkg/asset/quota/ibmcloud"
"github.com/openshift/installer/pkg/asset/quota/openstack"
"github.com/openshift/installer/pkg/diagnostics"
"github.com/openshift/installer/pkg/quota"
quotaaws "github.com/openshift/installer/pkg/quota/aws"
quotagcp "github.com/openshift/installer/pkg/quota/gcp"
ibmcloudconstraints "github.com/openshift/installer/pkg/quota/ibmcloud"
typesaws "github.com/openshift/installer/pkg/types/aws"
"github.com/openshift/installer/pkg/types/azure"
"github.com/openshift/installer/pkg/types/baremetal"
Expand Down Expand Up @@ -153,7 +156,23 @@ func (a *PlatformQuotaCheck) Generate(ctx context.Context, dependencies asset.Pa
if err != nil {
return errors.Wrap(err, "failed to create a new PISession")
}
case azure.Name, baremetal.Name, ibmcloud.Name, external.Name, none.Name, ovirt.Name, vsphere.Name, nutanix.Name:
case ibmcloud.Name:
client, err := ibmcloudic.NewClient(ic.Config.Platform.IBMCloud.ServiceEndpoints)
if err != nil {
logrus.Warnf("Failed to create IBM Cloud client for quota check, skipping: %v", err)
return nil
}
q, err := ibmcloudquota.Load(ctx, client, ic.Config)
if err != nil {
logrus.Warnf("Failed to load IBM Cloud quotas, skipping: %v", err)
return nil
}
reports, err := quota.Check(q, ibmcloudconstraints.Constraints(ic.Config, masters, workers))
if err != nil {
return summarizeFailingReport(reports)
}
summarizeReport(reports)
case azure.Name, baremetal.Name, external.Name, none.Name, ovirt.Name, vsphere.Name, nutanix.Name:
// no special provisioning requirements to check
default:
err = fmt.Errorf("unknown platform type %q", platform)
Expand Down
7 changes: 7 additions & 0 deletions pkg/quota/ibmcloud/OWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# See the OWNERS docs: https://git.k8s.io/community/contributors/guide/owners.md
# This file just uses aliases defined in OWNERS_ALIASES.

approvers:
- ibmcloud-approvers
reviewers:
- ibmcloud-reviewers
97 changes: 97 additions & 0 deletions pkg/quota/ibmcloud/ibmcloud.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package ibmcloud

import (
"sort"

machineapi "github.com/openshift/api/machine/v1beta1"
"github.com/openshift/installer/pkg/quota"
"github.com/openshift/installer/pkg/types"
)

// Constraints returns a list of quota constraints based on the InstallConfig.
// These constraints can be used to check if there is enough quota for creating
// a cluster for the install config.
func Constraints(config *types.InstallConfig, controlPlanes []machineapi.Machine, computes []machineapi.MachineSet) []quota.Constraint {
computeReplicas := make([]int64, len(computes))
for i, w := range computes {
computeReplicas[i] = int64(*w.Spec.Replicas)
}

var ret []quota.Constraint
for _, gen := range []constraintGenerator{
network(config, len(controlPlanes)),
instances(config, len(controlPlanes), computeReplicas),
} {
ret = append(ret, gen()...)
}
return aggregate(ret)
}

// constraintGenerator generates a list of constraints.
type constraintGenerator func() []quota.Constraint

func network(config *types.InstallConfig, controlPlaneCount int) func() []quota.Constraint {
return func() []quota.Constraint {
region := config.Platform.IBMCloud.Region

// Floating IPs: bootstrap + control plane nodes.
// The public API LB also uses floating IPs when publish is External.
fipCount := int64(1 + controlPlaneCount)
if config.Publish == types.ExternalPublishingStrategy || config.Publish == "" {
fipCount++
}

ret := []quota.Constraint{
{Name: "is/floating-ip", Region: region, Count: fipCount},
{Name: "is/security-group", Region: region, Count: 6},
{Name: "is/load-balancer", Region: region, Count: 2},
}

if config.Platform.IBMCloud.VPCName == "" {
ret = append(ret, quota.Constraint{
Name: "is/vpc", Region: region, Count: 1,
})
}

return ret
}
}

func instances(config *types.InstallConfig, controlPlaneCount int, computeReplicas []int64) func() []quota.Constraint {
return func() []quota.Constraint {
region := config.Platform.IBMCloud.Region

// control plane + bootstrap
count := int64(controlPlaneCount + 1)
for _, r := range computeReplicas {
count += r
}

return []quota.Constraint{
{Name: "is/instance", Region: region, Count: count},
}
}
}

func aggregate(constraints []quota.Constraint) []quota.Constraint {
if len(constraints) == 0 {
return nil
}

sort.SliceStable(constraints, func(i, j int) bool {
return constraints[i].Name < constraints[j].Name
})

i := 0
for j := 1; j < len(constraints); j++ {
if constraints[i].Name == constraints[j].Name && constraints[i].Region == constraints[j].Region {
constraints[i].Count += constraints[j].Count
} else {
i++
if i != j {
constraints[i] = constraints[j]
}
}
}
return constraints[:i+1]
}
60 changes: 60 additions & 0 deletions pkg/quota/ibmcloud/ibmcloud_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package ibmcloud

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"

"github.com/openshift/installer/pkg/quota"
)

func Test_aggregate(t *testing.T) {
cases := []struct {
input []quota.Constraint
exp []quota.Constraint
}{{
input: []quota.Constraint{
{Name: "is/instance", Region: "us-east", Count: 3},
{Name: "is/instance", Region: "us-east", Count: 1},
},
exp: []quota.Constraint{
{Name: "is/instance", Region: "us-east", Count: 4},
},
}, {
input: []quota.Constraint{
{Name: "is/floating-ip", Region: "us-east", Count: 5},
{Name: "is/instance", Region: "us-east", Count: 3},
{Name: "is/load-balancer", Region: "us-east", Count: 2},
{Name: "is/instance", Region: "us-east", Count: 1},
},
exp: []quota.Constraint{
{Name: "is/floating-ip", Region: "us-east", Count: 5},
{Name: "is/instance", Region: "us-east", Count: 4},
{Name: "is/load-balancer", Region: "us-east", Count: 2},
},
}, {
input: []quota.Constraint{
{Name: "is/floating-ip", Region: "us-east", Count: 5},
{Name: "is/instance", Region: "us-east", Count: 3},
{Name: "is/load-balancer", Region: "us-east", Count: 2},
{Name: "is/security-group", Region: "us-east", Count: 6},
},
exp: []quota.Constraint{
{Name: "is/floating-ip", Region: "us-east", Count: 5},
{Name: "is/instance", Region: "us-east", Count: 3},
{Name: "is/load-balancer", Region: "us-east", Count: 2},
{Name: "is/security-group", Region: "us-east", Count: 6},
},
}, {
input: nil,
exp: nil,
}}

for idx, test := range cases {
t.Run(fmt.Sprintf("test %d", idx), func(t *testing.T) {
got := aggregate(test.input)
assert.EqualValues(t, test.exp, got)
})
}
}