Skip to content

Commit 112f968

Browse files
authored
VPC: Add v2 support for VPC reconcile (#1886)
Add support to the v2 path to reconcile a VPC. Includes adding GlobalTagging support, and extending ResourceManager support.
1 parent b817136 commit 112f968

File tree

8 files changed

+514
-9
lines changed

8 files changed

+514
-9
lines changed

cloud/scope/vpc_cluster.go

Lines changed: 315 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@ import (
2424
"github.com/go-logr/logr"
2525

2626
"github.com/IBM/go-sdk-core/v5/core"
27+
"github.com/IBM/platform-services-go-sdk/globaltaggingv1"
2728
"github.com/IBM/platform-services-go-sdk/resourcecontrollerv2"
29+
"github.com/IBM/platform-services-go-sdk/resourcemanagerv2"
30+
"github.com/IBM/vpc-go-sdk/vpcv1"
2831

2932
"k8s.io/klog/v2/textlogger"
33+
"k8s.io/utils/ptr"
3034

3135
"sigs.k8s.io/controller-runtime/pkg/client"
3236

@@ -36,6 +40,7 @@ import (
3640
infrav1beta2 "sigs.k8s.io/cluster-api-provider-ibmcloud/api/v1beta2"
3741
"sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/authenticator"
3842
"sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/cos"
43+
"sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/globaltagging"
3944
"sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/resourcecontroller"
4045
"sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/resourcemanager"
4146
"sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/vpc"
@@ -65,6 +70,7 @@ type VPCClusterScope struct {
6570
patchHelper *patch.Helper
6671

6772
COSClient cos.Cos
73+
GlobalTaggingClient globaltagging.GlobalTagging
6874
ResourceControllerClient resourcecontroller.ResourceController
6975
ResourceManagerClient resourcemanager.ResourceManager
7076
VPCClient vpc.Vpc
@@ -117,27 +123,50 @@ func NewVPCClusterScope(params VPCClusterScopeParams) (*VPCClusterScope, error)
117123
}
118124

119125
// Create Global Tagging client.
120-
// TODO(cjschaef): need service support.
126+
gtOptions := globaltagging.ServiceOptions{
127+
GlobalTaggingV1Options: &globaltaggingv1.GlobalTaggingV1Options{
128+
Authenticator: auth,
129+
},
130+
}
131+
// Override the global tagging endpoint if provided.
132+
if gtEndpoint := endpoints.FetchEndpoints(string(endpoints.GlobalTagging), params.ServiceEndpoint); gtEndpoint != "" {
133+
gtOptions.URL = gtEndpoint
134+
params.Logger.V(3).Info("Overriding the default global tagging endpoint", "GlobaTaggingEndpoint", gtEndpoint)
135+
}
136+
globalTaggingClient, err := globaltagging.NewService(gtOptions)
137+
if err != nil {
138+
return nil, fmt.Errorf("failed to create global tagging client: %w", err)
139+
}
121140

122141
// Create Resource Controller client.
123142
rcOptions := resourcecontroller.ServiceOptions{
124143
ResourceControllerV2Options: &resourcecontrollerv2.ResourceControllerV2Options{
125144
Authenticator: auth,
126145
},
127146
}
128-
// Fetch the resource controller endpoint.
129-
rcEndpoint := endpoints.FetchEndpoints(string(endpoints.RC), params.ServiceEndpoint)
130-
if rcEndpoint != "" {
147+
// Override the resource controller endpoint if provided.
148+
if rcEndpoint := endpoints.FetchEndpoints(string(endpoints.RC), params.ServiceEndpoint); rcEndpoint != "" {
131149
rcOptions.URL = rcEndpoint
132150
params.Logger.V(3).Info("Overriding the default resource controller endpoint", "ResourceControllerEndpoint", rcEndpoint)
133151
}
134152
resourceControllerClient, err := resourcecontroller.NewService(rcOptions)
135153
if err != nil {
136-
return nil, fmt.Errorf("error failed to create resource controller client: %w", err)
154+
return nil, fmt.Errorf("failed to create resource controller client: %w", err)
137155
}
138156

139157
// Create Resource Manager client.
140-
// TODO(cjschaef): Need to extend ResourceManager service and endpoint support to add properly.
158+
rmOptions := &resourcemanagerv2.ResourceManagerV2Options{
159+
Authenticator: auth,
160+
}
161+
// Override the ResourceManager endpoint if provided.
162+
if rmEndpoint := endpoints.FetchEndpoints(string(endpoints.RM), params.ServiceEndpoint); rmEndpoint != "" {
163+
rmOptions.URL = rmEndpoint
164+
params.Logger.V(3).Info("Overriding the default resource manager endpoint", "ResourceManagerEndpoint", rmEndpoint)
165+
}
166+
resourceManagerClient, err := resourcemanager.NewService(rmOptions)
167+
if err != nil {
168+
return nil, fmt.Errorf("failed to create resource manager client: %w", err)
169+
}
141170

142171
clusterScope := &VPCClusterScope{
143172
Logger: params.Logger,
@@ -146,7 +175,9 @@ func NewVPCClusterScope(params VPCClusterScopeParams) (*VPCClusterScope, error)
146175
Cluster: params.Cluster,
147176
IBMVPCCluster: params.IBMVPCCluster,
148177
ServiceEndpoint: params.ServiceEndpoint,
178+
GlobalTaggingClient: globalTaggingClient,
149179
ResourceControllerClient: resourceControllerClient,
180+
ResourceManagerClient: resourceManagerClient,
150181
VPCClient: vpcClient,
151182
}
152183
return clusterScope, nil
@@ -166,3 +197,281 @@ func (s *VPCClusterScope) Close() error {
166197
func (s *VPCClusterScope) Name() string {
167198
return s.Cluster.Name
168199
}
200+
201+
// NetworkSpec returns the VPCClusterScope's Network spec.
202+
func (s *VPCClusterScope) NetworkSpec() *infrav1beta2.VPCNetworkSpec {
203+
return s.IBMVPCCluster.Spec.Network
204+
}
205+
206+
// NetworkStatus returns the VPCClusterScope's Network status.
207+
func (s *VPCClusterScope) NetworkStatus() *infrav1beta2.VPCNetworkStatus {
208+
return s.IBMVPCCluster.Status.Network
209+
}
210+
211+
// CheckTagExists checks whether a user tag already exists.
212+
func (s *VPCClusterScope) CheckTagExists(tagName string) (bool, error) {
213+
exists, err := s.GlobalTaggingClient.GetTagByName(tagName)
214+
if err != nil {
215+
return false, fmt.Errorf("failed checking for tag: %w", err)
216+
}
217+
return exists != nil, nil
218+
}
219+
220+
// GetNetworkResourceGroupID returns the Resource Group ID for the Network Resources if it is present. Otherwise, it defaults to the cluster's Resource Group ID.
221+
func (s *VPCClusterScope) GetNetworkResourceGroupID() (string, error) {
222+
// Check if the ID is available from Status first.
223+
if s.NetworkStatus() != nil && s.NetworkStatus().ResourceGroup != nil && s.NetworkStatus().ResourceGroup.ID != "" {
224+
return s.NetworkStatus().ResourceGroup.ID, nil
225+
}
226+
227+
// If there is no Network Resource Group defined, use the cluster's Resource Group.
228+
if s.NetworkSpec() == nil || s.NetworkSpec().ResourceGroup == nil {
229+
return s.GetResourceGroupID()
230+
}
231+
232+
// Otherwise, Collect the Network's Resource Group Id.
233+
// Retrieve the Resource Group based on the name.
234+
resourceGroup, err := s.ResourceManagerClient.GetResourceGroupByName(*s.NetworkSpec().ResourceGroup)
235+
if err != nil {
236+
return "", fmt.Errorf("failed to retrieve network resource group id by name: %w", err)
237+
} else if resourceGroup == nil || resourceGroup.ID == nil {
238+
return "", fmt.Errorf("error retrieving network resource group by name: %s", *s.NetworkSpec().ResourceGroup)
239+
}
240+
241+
// Populate the Network Status' Resource Group to shortcut future lookups.
242+
s.SetResourceStatus(infrav1beta2.ResourceTypeResourceGroup, &infrav1beta2.ResourceStatus{
243+
ID: *resourceGroup.ID,
244+
Name: s.NetworkSpec().ResourceGroup,
245+
Ready: true,
246+
})
247+
248+
return *resourceGroup.ID, nil
249+
}
250+
251+
// GetResourceGroupID returns the Resource Group ID for the cluster.
252+
func (s *VPCClusterScope) GetResourceGroupID() (string, error) {
253+
// Check if the Resource Group ID is available from Status first.
254+
if s.IBMVPCCluster.Status.ResourceGroup != nil && s.IBMVPCCluster.Status.ResourceGroup.ID != "" {
255+
return s.IBMVPCCluster.Status.ResourceGroup.ID, nil
256+
}
257+
258+
// If the Resource Group is not defined in Spec, we generate the name based on the cluster name.
259+
resourceGroupName := s.IBMVPCCluster.Spec.ResourceGroup
260+
if resourceGroupName == "" {
261+
resourceGroupName = s.IBMVPCCluster.Name
262+
}
263+
264+
// Retrieve the Resource Group based on the name.
265+
resourceGroup, err := s.ResourceManagerClient.GetResourceGroupByName(resourceGroupName)
266+
if err != nil {
267+
return "", fmt.Errorf("failed to retrieve resource group by name: %w", err)
268+
} else if resourceGroup == nil || resourceGroup.ID == nil {
269+
return "", fmt.Errorf("failed to find resource group by name: %s", resourceGroupName)
270+
}
271+
272+
// Populate the Stauts Resource Group to shortcut future lookups.
273+
s.SetResourceStatus(infrav1beta2.ResourceTypeResourceGroup, &infrav1beta2.ResourceStatus{
274+
ID: *resourceGroup.ID,
275+
Name: ptr.To(resourceGroupName),
276+
Ready: true,
277+
})
278+
279+
return *resourceGroup.ID, nil
280+
}
281+
282+
// GetServiceName returns the name of a given service type from Spec or generates a name for it.
283+
func (s *VPCClusterScope) GetServiceName(resourceType infrav1beta2.ResourceType) *string {
284+
switch resourceType {
285+
case infrav1beta2.ResourceTypeVPC:
286+
// Generate a name based off cluster name if no VPC defined in Spec, or no VPC name nor ID.
287+
if s.NetworkSpec().VPC == nil || (s.NetworkSpec().VPC.Name == nil && s.NetworkSpec().VPC.ID == nil) {
288+
return ptr.To(fmt.Sprintf("%s-vpc", s.Name()))
289+
}
290+
if s.NetworkSpec().VPC.Name != nil {
291+
return s.NetworkSpec().VPC.Name
292+
}
293+
default:
294+
s.V(3).Info("unsupported resource type", "resourceType", resourceType)
295+
}
296+
return nil
297+
}
298+
299+
// GetVPCID returns the VPC id, if available.
300+
func (s *VPCClusterScope) GetVPCID() (*string, error) {
301+
// Check if the VPC ID is available from Status first.
302+
if s.NetworkStatus() != nil && s.NetworkStatus().VPC != nil {
303+
return ptr.To(s.NetworkStatus().VPC.ID), nil
304+
}
305+
306+
if s.NetworkSpec() != nil && s.NetworkSpec().VPC != nil {
307+
if s.NetworkSpec().VPC.ID != nil {
308+
return s.NetworkSpec().VPC.ID, nil
309+
} else if s.NetworkSpec().VPC.Name != nil {
310+
vpcDetails, err := s.VPCClient.GetVPCByName(*s.NetworkSpec().VPC.Name)
311+
if err != nil {
312+
return nil, fmt.Errorf("failed vpc id lookup: %w", err)
313+
}
314+
315+
// Check if the VPC was found and has an ID
316+
if vpcDetails != nil && vpcDetails.ID != nil {
317+
// Set VPC ID in Status to shortcut future lookups
318+
s.SetResourceStatus(infrav1beta2.ResourceTypeVPC, &infrav1beta2.ResourceStatus{
319+
ID: *vpcDetails.ID,
320+
Name: s.NetworkSpec().VPC.Name,
321+
Ready: true,
322+
})
323+
}
324+
}
325+
}
326+
return nil, nil
327+
}
328+
329+
// SetResourceStatus sets the status for the provided ResourceType.
330+
func (s *VPCClusterScope) SetResourceStatus(resourceType infrav1beta2.ResourceType, resource *infrav1beta2.ResourceStatus) {
331+
// Ignore attempts to set status without resource.
332+
if resource == nil {
333+
return
334+
}
335+
s.V(3).Info("Setting status", "resourceType", resourceType, "resource", resource)
336+
switch resourceType {
337+
case infrav1beta2.ResourceTypeResourceGroup:
338+
if s.IBMVPCCluster.Status.ResourceGroup == nil {
339+
s.IBMVPCCluster.Status.ResourceGroup = resource
340+
return
341+
}
342+
s.IBMVPCCluster.Status.ResourceGroup.Set(*resource)
343+
case infrav1beta2.ResourceTypeVPC:
344+
if s.NetworkStatus() == nil {
345+
s.IBMVPCCluster.Status.Network = &infrav1beta2.VPCNetworkStatus{
346+
VPC: resource,
347+
}
348+
return
349+
} else if s.NetworkStatus().VPC == nil {
350+
s.IBMVPCCluster.Status.Network.VPC = resource
351+
}
352+
s.NetworkStatus().VPC.Set(*resource)
353+
default:
354+
s.V(3).Info("unsupported resource type", "resourceType", resourceType)
355+
}
356+
}
357+
358+
// TagResource will attach a user Tag to a resource.
359+
func (s *VPCClusterScope) TagResource(tagName string, resourceCRN string) error {
360+
// Verify the Tag we wish to use exists, otherwise create it.
361+
exists, err := s.CheckTagExists(tagName)
362+
if err != nil {
363+
return fmt.Errorf("failure checking if tag exists: %w", err)
364+
}
365+
366+
// Create tag if it doesn't exist.
367+
if !exists {
368+
createOptions := &globaltaggingv1.CreateTagOptions{}
369+
createOptions.SetTagNames([]string{tagName})
370+
if _, _, err := s.GlobalTaggingClient.CreateTag(createOptions); err != nil {
371+
return fmt.Errorf("failure creating tag: %w", err)
372+
}
373+
}
374+
375+
// Finally, tag resource.
376+
tagOptions := &globaltaggingv1.AttachTagOptions{}
377+
tagOptions.SetResources([]globaltaggingv1.Resource{
378+
{
379+
ResourceID: ptr.To(resourceCRN),
380+
},
381+
})
382+
tagOptions.SetTagName(tagName)
383+
tagOptions.SetTagType(globaltaggingv1.AttachTagOptionsTagTypeUserConst)
384+
385+
if _, _, err = s.GlobalTaggingClient.AttachTag(tagOptions); err != nil {
386+
return fmt.Errorf("failure tagging resource: %w", err)
387+
}
388+
389+
return nil
390+
}
391+
392+
// ReconcileVPC reconciles the cluster's VPC.
393+
func (s *VPCClusterScope) ReconcileVPC() (bool, error) {
394+
// If VPC id is set, that indicates the VPC already exists.
395+
vpcID, err := s.GetVPCID()
396+
if err != nil {
397+
return false, fmt.Errorf("failed to retrieve vpc id: %w", err)
398+
}
399+
if vpcID != nil {
400+
s.V(3).Info("VPC id is set", "id", vpcID)
401+
vpcDetails, _, err := s.VPCClient.GetVPC(&vpcv1.GetVPCOptions{
402+
ID: vpcID,
403+
})
404+
if err != nil {
405+
return false, fmt.Errorf("failed to retrieve vpc by id: %w", err)
406+
} else if vpcDetails == nil {
407+
return false, fmt.Errorf("failed to retrieve vpc with id: %s", *vpcID)
408+
}
409+
s.V(3).Info("Found VPC with provided id", "id", vpcID)
410+
411+
requeue := true
412+
if vpcDetails.Status != nil && *vpcDetails.Status == string(vpcv1.VPCStatusAvailableConst) {
413+
requeue = false
414+
}
415+
s.SetResourceStatus(infrav1beta2.ResourceTypeVPC, &infrav1beta2.ResourceStatus{
416+
ID: *vpcID,
417+
Name: vpcDetails.Name,
418+
// Ready status will be invert of the need to requeue.
419+
Ready: !requeue,
420+
})
421+
422+
// After updating the Status of VPC, return with requeue or return as reconcile complete.
423+
return requeue, nil
424+
}
425+
426+
// If no VPC id was found, we need to create a new VPC.
427+
s.V(3).Info("Creating a VPC")
428+
vpcDetails, err := s.createVPC()
429+
if err != nil {
430+
return false, fmt.Errorf("failed to create vpc: %w", err)
431+
}
432+
433+
s.V(3).Info("Successfully created VPC")
434+
var vpcName *string
435+
if vpcDetails != nil {
436+
vpcName = vpcDetails.Name
437+
}
438+
s.SetResourceStatus(infrav1beta2.ResourceTypeVPC, &infrav1beta2.ResourceStatus{
439+
ID: *vpcDetails.ID,
440+
Name: vpcName,
441+
Ready: false,
442+
})
443+
return true, nil
444+
}
445+
446+
func (s *VPCClusterScope) createVPC() (*vpcv1.VPC, error) {
447+
// We use the cluster's Resource Group ID, as we expect to create all resources in that Resource Group.
448+
resourceGroupID, err := s.GetResourceGroupID()
449+
if err != nil {
450+
return nil, fmt.Errorf("failed retreiving resource group id during vpc creation: %w", err)
451+
} else if resourceGroupID == "" {
452+
return nil, fmt.Errorf("resource group id is empty cannot create vpc")
453+
}
454+
vpcName := s.GetServiceName(infrav1beta2.ResourceTypeVPC)
455+
if s.NetworkSpec() != nil && s.NetworkSpec().VPC != nil && s.NetworkSpec().VPC.Name != nil {
456+
vpcName = s.NetworkSpec().VPC.Name
457+
}
458+
459+
// TODO(cjschaef): Look at adding support to specify prefix management
460+
addressPrefixManagement := "auto"
461+
vpcOptions := &vpcv1.CreateVPCOptions{
462+
AddressPrefixManagement: &addressPrefixManagement,
463+
Name: vpcName,
464+
ResourceGroup: &vpcv1.ResourceGroupIdentity{ID: &resourceGroupID},
465+
}
466+
vpcDetails, _, err := s.VPCClient.CreateVPC(vpcOptions)
467+
if err != nil {
468+
return nil, fmt.Errorf("error creating vpc: %w", err)
469+
} else if vpcDetails == nil {
470+
return nil, fmt.Errorf("no vpc details after creation")
471+
}
472+
if err = s.TagResource(s.IBMVPCCluster.Name, *vpcDetails.CRN); err != nil {
473+
return nil, fmt.Errorf("error tagging vpc: %w", err)
474+
}
475+
476+
return vpcDetails, nil
477+
}

0 commit comments

Comments
 (0)