-
Notifications
You must be signed in to change notification settings - Fork 84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support VPC security group for PowerVS clusters #1738
Support VPC security group for PowerVS clusters #1738
Conversation
✅ Deploy Preview for kubernetes-sigs-cluster-api-ibmcloud ready!
To edit notification comments on pull requests, go to your Netlify site configuration. |
218dc9c
to
fc926e4
Compare
/cc @Karthik-K-N |
/cc @cjschaef |
@Karthik-K-N: GitHub didn't allow me to request PR reviews from the following users: cjschaef. Note that only kubernetes-sigs members and repo collaborators can review this PR, and authors cannot review their own PRs. In response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
cloud/scope/powervs_cluster.go
Outdated
return true, fmt.Errorf("error validating existing security group: %v", err) | ||
} | ||
if securityGroupID != nil { | ||
s.Logger.Info("Security group already exists", "name", *securityGroup.Name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
securityGroup.Name
can be nil, so its better to handle it everywhere before directly de referring it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactored it to get it from vpcv1.SecurityGroup
where name is a required param.
cloud/scope/powervs_cluster.go
Outdated
ControllerCreated: ptr.To(false), | ||
}) | ||
|
||
return false, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think in case of multiple security groups we need to continue to process next item instead of returning from here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, Done
thanks!
cloud/scope/powervs_cluster.go
Outdated
} | ||
|
||
s.SetVPCSecurityGroupID(*securityGroup.Name, infrav1beta2.ResourceReference{ | ||
ID: securityGroupID, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should update the status as soon as we create rather than doing it after creating security group rule as it might fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please have a look at the latest implementation, added ruleIDs also in status.
cloud/scope/powervs_cluster.go
Outdated
|
||
options := &vpcv1.CreateSecurityGroupOptions{ | ||
VPC: &vpcv1.VPCIdentity{ | ||
ID: s.GetVPCID(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s.GetVPCID() can be nil, So lets handle it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As per the reconcile order, when it reaches here VPC status would have been populated right?
Do we really need to check? If its not populated, it would requeue on ReconcileVPC only I believe.
cloud/scope/powervs_cluster.go
Outdated
if cidrSubnet == nil { | ||
return fmt.Errorf("subnet by name '%s' not exist", *remote.CIDRSubnetName) | ||
} | ||
s.Logger.Info("Creating security group rule", "securityGroupID", *securityGroupID, "direction", *direction, "protocol", *protocol, "cidrBlockSubnet", *remote.CIDRSubnetName, "cidr", *cidrSubnet.Ipv4CIDRBlock) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May be lets use V level logs to dump detailed infos
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
cloud/scope/powervs_cluster.go
Outdated
return false, fmt.Errorf("error validating security group rule: %v", err) | ||
} | ||
if !match { | ||
return false, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about logging with V(3) with which security group rule did not match.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's really difficult to get that since it's not that straight forward. Let's keep it like this for now!
cloud/scope/powervs_cluster.go
Outdated
// validateSecurityGroupRule compares a specific security group's rule with the spec and existing security group's rule. | ||
func (s *PowerVSClusterScope) validateSecurityGroupRule(originalSecurityGroup vpcv1.SecurityGroup, direction infrav1beta2.SecurityGroupRuleDirection, protocol string, portMin, portMax int64, icmpCode, icmpType *string, remote infrav1beta2.SecurityGroupRuleRemote) (bool, error) { | ||
var match bool | ||
var err error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about declaring them in return parameters?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
cloud/scope/powervs_cluster.go
Outdated
} | ||
} | ||
} | ||
if match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this should be !match? because we should validate all the rules, If the first rule matches we should not return right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we do not return and attempt to validate only one rule matches.
I expect this function is attempting to verify only one SecurityGroupRule definition from Spec
exists in the SecurityGroup. And the function (validateSecurityGroupRule
) gets called once for each SecurityGroupRule defined (per SecurityGroup).
So exiting here makes sense IMO, if we only want to validate that a SG Rule exists (expecting we wouldn't have duplicates.....which I don't know if the API allows that).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes its trying to match that particular rule passed is exists or not. So exiting as soon as it finds a match.
cloud/scope/powervs_cluster.go
Outdated
|
||
// validateVPCSecurityGroup validates the security group and it's rules. | ||
func (s *PowerVSClusterScope) validateVPCSecurityGroup(securityGroup infrav1beta2.SecurityGroup) (*string, error) { | ||
securityGroupDet, err := s.IBMVPCClient.GetSecurityGroupByName(*securityGroup.Name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May we can try to fetch by id as well, if id is already set in status? also we need to make sure name is not nil.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes agree, refactored, ptal!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The design makes sense, handling the complexities of SecurityGroups and SecurityGroupRules definitions.
In my mind, I was thinking VPC would iterate through creating all the SecurityGroups first, then return to populate the SecurityGroupRules, as some of the Remotes depend on the SecurityGroups (CRN). I will rethink the process some compared with the expectations here, if this looping is acceptable, when to raise errors, etc. due to these additional complexities (inter SecurityGroup/SecurityGroupRule dependencies).
cloud/scope/powervs_cluster.go
Outdated
if _, _, err := s.IBMVPCClient.GetSecurityGroup(&vpcv1.GetSecurityGroupOptions{ | ||
ID: securityGroup.ID, | ||
}); err != nil { | ||
if strings.Contains(err.Error(), "not found") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't the detailedResponse provide an Http StatusCode you could check rather than rely on the error text?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it would, but this method of validating the resource deletion is followed through out this controller code.
IMO also it's fine.
pkg/cloud/services/vpc/service.go
Outdated
if start != "" { | ||
listSecurityGroupOptions.Start = &start | ||
} | ||
securityGroupCollection, _, err := s.ListSecurityGroups(&listSecurityGroupOptions) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we instead use the vpcv1.SecurityGroupsPager
functionality instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the suggestion, implemented using pager.
cloud/scope/powervs_cluster.go
Outdated
} | ||
} | ||
} | ||
if match { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps we do not return and attempt to validate only one rule matches.
I expect this function is attempting to verify only one SecurityGroupRule definition from Spec
exists in the SecurityGroup. And the function (validateSecurityGroupRule
) gets called once for each SecurityGroupRule defined (per SecurityGroup).
So exiting here makes sense IMO, if we only want to validate that a SG Rule exists (expecting we wouldn't have duplicates.....which I don't know if the API allows that).
efc2b5f
to
80acbd1
Compare
@cjschaef Also I have refactored few things from last review, ptal! |
/retest-required |
2ed95eb
to
11743b2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, Few questions otherwise, Overall LGTM
setRemote := func(remote infrav1beta2.SecurityGroupRuleRemote, remoteOption *vpcv1.SecurityGroupRuleRemotePrototype) error { | ||
switch remote.RemoteType { | ||
case infrav1beta2.SecurityGroupRuleRemoteTypeCIDR: | ||
cidrSubnet, err := s.IBMVPCClient.GetVPCSubnetByName(*remote.CIDRSubnetName) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of now do we have any check which mandates the user to set remote.CIDRSubnetName
when type is infrav1beta2.SecurityGroupRuleRemoteTypeCIDR
, If not lets create an issue to add a wehbook later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this is validated in kubebuilder rules in spec.
cloud/scope/powervs_cluster.go
Outdated
// ReconcileVPCSecurityGroup reconciles VPC security group. | ||
func (s *PowerVSClusterScope) ReconcileVPCSecurityGroup() (bool, error) { | ||
for _, securityGroup := range s.IBMPowerVSCluster.Spec.VPCSecurityGroups { | ||
securityGroupID, securityGroupRuleIDs := s.GetVPCSecurityGroupID(*securityGroup.Name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets carefully handle *securityGroup.Name
when user sets securityGroup.ID
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, taken care, please check!
11743b2
to
fd93a8d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@mkumatag @Prajyot-Parab @Amulyam24 Please take a look. Thanks
cloud/scope/powervs_cluster.go
Outdated
SecurityGroupID: securityGroupID, | ||
ID: rule, | ||
}); err != nil { | ||
return true, err |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dharaneeshvrd, shouldn't we be returning false
on err and not requeue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The requeue was added for handling the state of the infra resources, maybe it can be skipped if security group doesn't need state management?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes Agree, corrected.
Thanks @Amulyam24 !
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dharaneesh, since we are returning false every time, maybe we can just return err and remove the requeue check in the controller?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Corrected!
fd93a8d
to
065ffc0
Compare
cloud/scope/powervs_cluster.go
Outdated
return false, fmt.Errorf("error validating existing security group: %v", err) | ||
} | ||
if securityGroupDet != nil { | ||
s.Logger.Info("Security group already exists", "name", *securityGroupDet.Name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we set log level to 3 wherever applicable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think its ok here, since its going to this block only one time. Same is there on other components too when user provided spec is already exists in cloud, wdys?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cloud/scope/powervs_cluster.go
Outdated
if _, _, err := s.IBMVPCClient.GetSecurityGroup(&vpcv1.GetSecurityGroupOptions{ | ||
ID: securityGroup.ID, | ||
}); err != nil { | ||
if strings.Contains(err.Error(), "not found") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add not found
as a constant in types.go?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
cloud/scope/powervs_cluster.go
Outdated
}); err != nil { | ||
if strings.Contains(err.Error(), "not found") { | ||
s.Info("VPC security group successfully deleted", "ID", securityGroup.ID) | ||
return false, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same comment applies here - please check if returning err would be enough and requeue can be avoided if not needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes true, I followed the vpc subnet deletion, but I think its bug to return from the loop after processing one item here ;)
065ffc0
to
d94ef4a
Compare
d94ef4a
to
ad48c87
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dharaneeshvrd, couple of things, with log refactoring changes, do you mind following the below in this PR:
- consistent naming for VPC and infra resources in logs such as VPC, VPC subnet, VPC security group etc.
- Error messages starting with
failed to...
Apart from minor comments, overall LGTM
ControllerCreated: ptr.To(true), | ||
}) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we log a statement on successful creation of a security group?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
ad48c87
to
ad07a5b
Compare
@Amulyam24 |
ad07a5b
to
36ce8ea
Compare
Sure @dharaneeshvrd, I can take care of them in the logging PR once this is merged. Changes LGTM! |
@@ -103,6 +103,10 @@ type IBMPowerVSClusterSpec struct { | |||
// +optional | |||
VPCSubnets []Subnet `json:"vpcSubnets,omitempty"` | |||
|
|||
// securityGroups to attach it to the VPC resource | |||
// +optional | |||
VPCSecurityGroups []SecurityGroup `json:"vpcSecurityGroups,omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any specific reason for not naming this as VPCSecurityGroup
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since its a list item, named it in plural just like VPCSubnets
and LoadBalancers
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
n/m, this SecurityGroup
struct is created earlier in the types.go file
api/v1beta2/ibmvpccluster_types.go
Outdated
// id represents the id of the resource. | ||
ID *string `json:"id,omitempty"` | ||
// rules contains the id of rules created under the security group | ||
RuleIDs []*string `json:"rules,omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Its always good to keep both the struct fieldname and the json name same.
cloud/scope/powervs_cluster.go
Outdated
@@ -494,6 +496,44 @@ func (s *PowerVSClusterScope) SetVPCSubnetID(name string, resource infrav1beta2. | |||
s.IBMPowerVSCluster.Status.VPCSubnet[name] = resource | |||
} | |||
|
|||
// GetVPCSecurityGroupID returns the VPC security group id. | |||
func (s *PowerVSClusterScope) GetVPCSecurityGroupID(name string) (*string, []*string) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
function name says GetVPCSecurityGroupID
but returning both security group id and also the rules.
func (s *PowerVSClusterScope) GetVPCSecurityGroupID(name string) (*string, []*string) { | |
func (s *PowerVSClusterScope) GetVPCSecurityGroup(name string) (*string, []*string) { |
cloud/scope/powervs_cluster.go
Outdated
} | ||
|
||
// GetVPCSecurityGroupRuleIDs returns the VPC security group's ruleIDs. | ||
func (s *PowerVSClusterScope) GetVPCSecurityGroupRuleIDs(securityGroupID string) []*string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is more or less same as GetVPCSecurityGroupID
but only difference is that we are supplying the id as param
we can keep the functions like GetVPCSecurityGroupByName
and GetVPCSecurityGroupByID
with returning same content.
cloud/scope/powervs_cluster.go
Outdated
} | ||
|
||
// validateVPCSecurityGroupRules compares a specific security group rules spec with the existing security group's rules. | ||
func (s *PowerVSClusterScope) validateVPCSecurityGroupRules(originalSecurityGroup *vpcv1.SecurityGroup, expectedSecurityGroup infrav1beta2.SecurityGroup) ([]*string, bool, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we care just about rules as a param, then lets send only rules as a param
ruleIDs, ok, err := s.validateVPCSecurityGroupRules(securityGroupDet, securityGroup.Rules)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment applies for other functions as well.
cloud/scope/powervs_cluster.go
Outdated
switch direction { | ||
case infrav1beta2.SecurityGroupRuleDirectionInbound: | ||
protocol = string(expectedRule.Source.Protocol) | ||
portMin = int64(expectedRule.Source.PortRange.MinimumPort) | ||
portMax = int64(expectedRule.Source.PortRange.MaximumPort) | ||
icmpCode := expectedRule.Source.ICMPCode | ||
icmpType := expectedRule.Source.ICMPType | ||
|
||
for _, remote := range expectedRule.Source.Remotes { | ||
id, match, err := s.validateSecurityGroupRule(originalSecurityGroup, direction, protocol, portMin, portMax, icmpCode, icmpType, remote) | ||
if err != nil { | ||
return nil, false, fmt.Errorf("failed to validate security group rule: %w", err) | ||
} | ||
if !match { | ||
return nil, false, nil | ||
} | ||
ruleIDs = append(ruleIDs, id) | ||
} | ||
case infrav1beta2.SecurityGroupRuleDirectionOutbound: | ||
protocol = string(expectedRule.Destination.Protocol) | ||
portMin = int64(expectedRule.Destination.PortRange.MinimumPort) | ||
portMax = int64(expectedRule.Destination.PortRange.MaximumPort) | ||
icmpCode := expectedRule.Destination.ICMPCode | ||
icmpType := expectedRule.Destination.ICMPType | ||
|
||
for _, remote := range expectedRule.Destination.Remotes { | ||
id, match, err := s.validateSecurityGroupRule(originalSecurityGroup, direction, protocol, portMin, portMax, icmpCode, icmpType, remote) | ||
if err != nil { | ||
return nil, false, fmt.Errorf("failed to validate security group rule: %v", err) | ||
} | ||
if !match { | ||
return nil, false, nil | ||
} | ||
ruleIDs = append(ruleIDs, id) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
except few, there is lots of common code, wondering if we can reuse some part of it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
applies to other functions as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tried to reuse some parts, ptal now!
cloud/scope/powervs_cluster.go
Outdated
} | ||
} | ||
if match { | ||
break |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can directly return from here instead.
cloud/scope/powervs_cluster.go
Outdated
match = true | ||
} | ||
case infrav1beta2.SecurityGroupRuleRemoteTypeIP: | ||
if originalSGRemote.Address != nil && *originalSGRemote.Address == *expectedSGRemote.IP { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why the field name is IP
? do we need to rename this to Address
orig:
IP *string `json:"ip,omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
renamed to Address to keep it in sync with the VPC's field.
return false, fmt.Errorf("failed to find vpc subnet by name '%s' for getting cidr block: %w", *expectedSGRemote.CIDRSubnetName, err) | ||
} | ||
|
||
if originalSGRemote.CIDRBlock != nil && cidrSubnet != nil && *originalSGRemote.CIDRBlock == *cidrSubnet.Ipv4CIDRBlock { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and also here
IP *string `json:"ip,omitempty"` |
8f10554
to
ddaf357
Compare
Addressed your comments, ptal now @mkumatag |
3b1b413
to
c417314
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall LGTM
abd7bea
to
e328d9c
Compare
e328d9c
to
687d1d6
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/lgtm
[APPROVALNOTIFIER] This PR is APPROVED This pull-request has been approved by: dharaneeshvrd, mkumatag The full list of commands accepted by this bot can be found here. The pull request process is described here
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
What this PR does / why we need it:
Add support for VPC security group for PowerVS clusters
Which issue(s) this PR fixes (optional, in
fixes #<issue number>(, fixes #<issue_number>, ...)
format, will close the issue(s) when PR gets merged):Fixes #
Fixes #1706
Special notes for your reviewer:
/area provider/ibmcloud
Release note: