Skip to content
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

VPC: Add v2 support for VPC reconcile #1886

Merged
merged 1 commit into from
Jul 23, 2024
Merged
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
321 changes: 315 additions & 6 deletions cloud/scope/vpc_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ import (
"github.com/go-logr/logr"

"github.com/IBM/go-sdk-core/v5/core"
"github.com/IBM/platform-services-go-sdk/globaltaggingv1"
"github.com/IBM/platform-services-go-sdk/resourcecontrollerv2"
"github.com/IBM/platform-services-go-sdk/resourcemanagerv2"
"github.com/IBM/vpc-go-sdk/vpcv1"

"k8s.io/klog/v2/textlogger"
"k8s.io/utils/ptr"

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

Expand All @@ -36,6 +40,7 @@ import (
infrav1beta2 "sigs.k8s.io/cluster-api-provider-ibmcloud/api/v1beta2"
"sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/authenticator"
"sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/cos"
"sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/globaltagging"
"sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/resourcecontroller"
"sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/resourcemanager"
"sigs.k8s.io/cluster-api-provider-ibmcloud/pkg/cloud/services/vpc"
Expand Down Expand Up @@ -65,6 +70,7 @@ type VPCClusterScope struct {
patchHelper *patch.Helper

COSClient cos.Cos
GlobalTaggingClient globaltagging.GlobalTagging
ResourceControllerClient resourcecontroller.ResourceController
ResourceManagerClient resourcemanager.ResourceManager
VPCClient vpc.Vpc
Expand Down Expand Up @@ -117,27 +123,50 @@ func NewVPCClusterScope(params VPCClusterScopeParams) (*VPCClusterScope, error)
}

// Create Global Tagging client.
// TODO(cjschaef): need service support.
gtOptions := globaltagging.ServiceOptions{
GlobalTaggingV1Options: &globaltaggingv1.GlobalTaggingV1Options{
Authenticator: auth,
},
}
// Override the global tagging endpoint if provided.
if gtEndpoint := endpoints.FetchEndpoints(string(endpoints.GlobalTagging), params.ServiceEndpoint); gtEndpoint != "" {
gtOptions.URL = gtEndpoint
params.Logger.V(3).Info("Overriding the default global tagging endpoint", "GlobaTaggingEndpoint", gtEndpoint)
}
globalTaggingClient, err := globaltagging.NewService(gtOptions)
if err != nil {
return nil, fmt.Errorf("failed to create global tagging client: %w", err)
}

// Create Resource Controller client.
rcOptions := resourcecontroller.ServiceOptions{
ResourceControllerV2Options: &resourcecontrollerv2.ResourceControllerV2Options{
Authenticator: auth,
},
}
// Fetch the resource controller endpoint.
rcEndpoint := endpoints.FetchEndpoints(string(endpoints.RC), params.ServiceEndpoint)
if rcEndpoint != "" {
// Override the resource controller endpoint if provided.
if rcEndpoint := endpoints.FetchEndpoints(string(endpoints.RC), params.ServiceEndpoint); rcEndpoint != "" {
rcOptions.URL = rcEndpoint
params.Logger.V(3).Info("Overriding the default resource controller endpoint", "ResourceControllerEndpoint", rcEndpoint)
}
resourceControllerClient, err := resourcecontroller.NewService(rcOptions)
if err != nil {
return nil, fmt.Errorf("error failed to create resource controller client: %w", err)
return nil, fmt.Errorf("failed to create resource controller client: %w", err)
}

// Create Resource Manager client.
// TODO(cjschaef): Need to extend ResourceManager service and endpoint support to add properly.
rmOptions := &resourcemanagerv2.ResourceManagerV2Options{
Authenticator: auth,
}
// Override the ResourceManager endpoint if provided.
if rmEndpoint := endpoints.FetchEndpoints(string(endpoints.RM), params.ServiceEndpoint); rmEndpoint != "" {
rmOptions.URL = rmEndpoint
params.Logger.V(3).Info("Overriding the default resource manager endpoint", "ResourceManagerEndpoint", rmEndpoint)
}
resourceManagerClient, err := resourcemanager.NewService(rmOptions)
if err != nil {
return nil, fmt.Errorf("failed to create resource manager client: %w", err)
}

clusterScope := &VPCClusterScope{
Logger: params.Logger,
Expand All @@ -146,7 +175,9 @@ func NewVPCClusterScope(params VPCClusterScopeParams) (*VPCClusterScope, error)
Cluster: params.Cluster,
IBMVPCCluster: params.IBMVPCCluster,
ServiceEndpoint: params.ServiceEndpoint,
GlobalTaggingClient: globalTaggingClient,
ResourceControllerClient: resourceControllerClient,
ResourceManagerClient: resourceManagerClient,
VPCClient: vpcClient,
}
return clusterScope, nil
Expand All @@ -166,3 +197,281 @@ func (s *VPCClusterScope) Close() error {
func (s *VPCClusterScope) Name() string {
return s.Cluster.Name
}

// NetworkSpec returns the VPCClusterScope's Network spec.
func (s *VPCClusterScope) NetworkSpec() *infrav1beta2.VPCNetworkSpec {
return s.IBMVPCCluster.Spec.Network
}

// NetworkStatus returns the VPCClusterScope's Network status.
func (s *VPCClusterScope) NetworkStatus() *infrav1beta2.VPCNetworkStatus {
return s.IBMVPCCluster.Status.Network
}

// CheckTagExists checks whether a user tag already exists.
func (s *VPCClusterScope) CheckTagExists(tagName string) (bool, error) {
exists, err := s.GlobalTaggingClient.GetTagByName(tagName)
if err != nil {
return false, fmt.Errorf("failed checking for tag: %w", err)
}
return exists != nil, nil
}

// 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.
if s.NetworkStatus() != nil && s.NetworkStatus().ResourceGroup != nil && s.NetworkStatus().ResourceGroup.ID != "" {
return s.NetworkStatus().ResourceGroup.ID, nil
}

// If there is no Network Resource Group defined, use the cluster's Resource Group.
if s.NetworkSpec() == nil || s.NetworkSpec().ResourceGroup == nil {
return s.GetResourceGroupID()
}

// Otherwise, Collect the Network's Resource Group Id.
// Retrieve the Resource Group based on the name.
resourceGroup, err := s.ResourceManagerClient.GetResourceGroupByName(*s.NetworkSpec().ResourceGroup)
if err != nil {
return "", fmt.Errorf("failed to retrieve network resource group id by name: %w", err)
} else if resourceGroup == nil || resourceGroup.ID == nil {
return "", fmt.Errorf("error retrieving network resource group by name: %s", *s.NetworkSpec().ResourceGroup)
}

// Populate the Network Status' Resource Group to shortcut future lookups.
s.SetResourceStatus(infrav1beta2.ResourceTypeResourceGroup, &infrav1beta2.ResourceStatus{
ID: *resourceGroup.ID,
Name: s.NetworkSpec().ResourceGroup,
Ready: true,
})

return *resourceGroup.ID, nil
}

// GetResourceGroupID returns the Resource Group ID for the cluster.
func (s *VPCClusterScope) GetResourceGroupID() (string, error) {
// Check if the Resource Group ID is available from Status first.
if s.IBMVPCCluster.Status.ResourceGroup != nil && s.IBMVPCCluster.Status.ResourceGroup.ID != "" {
return s.IBMVPCCluster.Status.ResourceGroup.ID, nil
}

// If the Resource Group is not defined in Spec, we generate the name based on the cluster name.
resourceGroupName := s.IBMVPCCluster.Spec.ResourceGroup
if resourceGroupName == "" {
resourceGroupName = s.IBMVPCCluster.Name
}

// Retrieve the Resource Group based on the name.
resourceGroup, err := s.ResourceManagerClient.GetResourceGroupByName(resourceGroupName)
if err != nil {
return "", fmt.Errorf("failed to retrieve resource group by name: %w", err)
} else if resourceGroup == nil || resourceGroup.ID == nil {
return "", fmt.Errorf("failed to find resource group by name: %s", resourceGroupName)
}

// Populate the Stauts Resource Group to shortcut future lookups.
s.SetResourceStatus(infrav1beta2.ResourceTypeResourceGroup, &infrav1beta2.ResourceStatus{
ID: *resourceGroup.ID,
Name: ptr.To(resourceGroupName),
Ready: true,
})

return *resourceGroup.ID, nil
}

// GetServiceName returns the name of a given service type from Spec or generates a name for it.
func (s *VPCClusterScope) GetServiceName(resourceType infrav1beta2.ResourceType) *string {
switch resourceType {
case infrav1beta2.ResourceTypeVPC:
// Generate a name based off cluster name if no VPC defined in Spec, or no VPC name nor ID.
if s.NetworkSpec().VPC == nil || (s.NetworkSpec().VPC.Name == nil && s.NetworkSpec().VPC.ID == nil) {
return ptr.To(fmt.Sprintf("%s-vpc", s.Name()))
}
if s.NetworkSpec().VPC.Name != nil {
return s.NetworkSpec().VPC.Name
}
default:
s.V(3).Info("unsupported resource type", "resourceType", resourceType)
}
return nil
}

// GetVPCID returns the VPC id, if available.
func (s *VPCClusterScope) GetVPCID() (*string, error) {
// Check if the VPC ID is available from Status first.
if s.NetworkStatus() != nil && s.NetworkStatus().VPC != nil {
return ptr.To(s.NetworkStatus().VPC.ID), nil
}

if s.NetworkSpec() != nil && s.NetworkSpec().VPC != nil {
if s.NetworkSpec().VPC.ID != nil {
return s.NetworkSpec().VPC.ID, nil
} else if s.NetworkSpec().VPC.Name != nil {
vpcDetails, err := s.VPCClient.GetVPCByName(*s.NetworkSpec().VPC.Name)
if err != nil {
return nil, fmt.Errorf("failed vpc id lookup: %w", err)
}

// Check if the VPC was found and has an ID
if vpcDetails != nil && vpcDetails.ID != nil {
// Set VPC ID in Status to shortcut future lookups
s.SetResourceStatus(infrav1beta2.ResourceTypeVPC, &infrav1beta2.ResourceStatus{
ID: *vpcDetails.ID,
Name: s.NetworkSpec().VPC.Name,
Ready: true,
})
}
}
}
return nil, nil
}

// SetResourceStatus sets the status for the provided ResourceType.
func (s *VPCClusterScope) SetResourceStatus(resourceType infrav1beta2.ResourceType, resource *infrav1beta2.ResourceStatus) {
// Ignore attempts to set status without resource.
if resource == nil {
return
}
s.V(3).Info("Setting status", "resourceType", resourceType, "resource", resource)
switch resourceType {
case infrav1beta2.ResourceTypeResourceGroup:
if s.IBMVPCCluster.Status.ResourceGroup == nil {
s.IBMVPCCluster.Status.ResourceGroup = resource
return
}
s.IBMVPCCluster.Status.ResourceGroup.Set(*resource)
case infrav1beta2.ResourceTypeVPC:
if s.NetworkStatus() == nil {
s.IBMVPCCluster.Status.Network = &infrav1beta2.VPCNetworkStatus{
VPC: resource,
}
return
} else if s.NetworkStatus().VPC == nil {
s.IBMVPCCluster.Status.Network.VPC = resource
}
s.NetworkStatus().VPC.Set(*resource)
default:
s.V(3).Info("unsupported resource type", "resourceType", resourceType)
}
}

// TagResource will attach a user Tag to a resource.
func (s *VPCClusterScope) TagResource(tagName string, resourceCRN string) error {
// Verify the Tag we wish to use exists, otherwise create it.
exists, err := s.CheckTagExists(tagName)
if err != nil {
return fmt.Errorf("failure checking if tag exists: %w", err)
}

// Create tag if it doesn't exist.
if !exists {
createOptions := &globaltaggingv1.CreateTagOptions{}
createOptions.SetTagNames([]string{tagName})
if _, _, err := s.GlobalTaggingClient.CreateTag(createOptions); err != nil {
return fmt.Errorf("failure creating tag: %w", err)
}
}

// Finally, tag resource.
tagOptions := &globaltaggingv1.AttachTagOptions{}
tagOptions.SetResources([]globaltaggingv1.Resource{
{
ResourceID: ptr.To(resourceCRN),
},
})
tagOptions.SetTagName(tagName)
tagOptions.SetTagType(globaltaggingv1.AttachTagOptionsTagTypeUserConst)

if _, _, err = s.GlobalTaggingClient.AttachTag(tagOptions); err != nil {
return fmt.Errorf("failure tagging resource: %w", err)
}

return nil
}

// ReconcileVPC reconciles the cluster's VPC.
func (s *VPCClusterScope) ReconcileVPC() (bool, error) {
// If VPC id is set, that indicates the VPC already exists.
vpcID, err := s.GetVPCID()
if err != nil {
return false, fmt.Errorf("failed to retrieve vpc id: %w", err)
}
if vpcID != nil {
s.V(3).Info("VPC id is set", "id", vpcID)
vpcDetails, _, err := s.VPCClient.GetVPC(&vpcv1.GetVPCOptions{
ID: vpcID,
})
if err != nil {
return false, fmt.Errorf("failed to retrieve vpc by id: %w", err)
} else if vpcDetails == nil {
return false, fmt.Errorf("failed to retrieve vpc with id: %s", *vpcID)
}
s.V(3).Info("Found VPC with provided id", "id", vpcID)

requeue := true
if vpcDetails.Status != nil && *vpcDetails.Status == string(vpcv1.VPCStatusAvailableConst) {
requeue = false
}
s.SetResourceStatus(infrav1beta2.ResourceTypeVPC, &infrav1beta2.ResourceStatus{
ID: *vpcID,
Name: vpcDetails.Name,
// Ready status will be invert of the need to requeue.
Ready: !requeue,
})

// After updating the Status of VPC, return with requeue or return as reconcile complete.
return requeue, nil
}

// If no VPC id was found, we need to create a new VPC.
s.V(3).Info("Creating a VPC")
vpcDetails, err := s.createVPC()
if err != nil {
return false, fmt.Errorf("failed to create vpc: %w", err)
}

s.V(3).Info("Successfully created VPC")
var vpcName *string
if vpcDetails != nil {
vpcName = vpcDetails.Name
}
s.SetResourceStatus(infrav1beta2.ResourceTypeVPC, &infrav1beta2.ResourceStatus{
ID: *vpcDetails.ID,
Name: vpcName,
Ready: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think IIUC in the next iteration you are expecting Ready to be set to true based on Status.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is my expectation, to update the status to Ready on the next round of reconcile (to make sure it is actually found via VPC API (GetVPC) call.

})
return true, nil
}

func (s *VPCClusterScope) createVPC() (*vpcv1.VPC, error) {
// We use the cluster's Resource Group ID, as we expect to create all resources in that Resource Group.
resourceGroupID, err := s.GetResourceGroupID()
if err != nil {
return nil, fmt.Errorf("failed retreiving resource group id during vpc creation: %w", err)
} else if resourceGroupID == "" {
return nil, fmt.Errorf("resource group id is empty cannot create vpc")
}
vpcName := s.GetServiceName(infrav1beta2.ResourceTypeVPC)
if s.NetworkSpec() != nil && s.NetworkSpec().VPC != nil && s.NetworkSpec().VPC.Name != nil {
vpcName = s.NetworkSpec().VPC.Name
}

// TODO(cjschaef): Look at adding support to specify prefix management
addressPrefixManagement := "auto"
vpcOptions := &vpcv1.CreateVPCOptions{
AddressPrefixManagement: &addressPrefixManagement,
Name: vpcName,
ResourceGroup: &vpcv1.ResourceGroupIdentity{ID: &resourceGroupID},
}
vpcDetails, _, err := s.VPCClient.CreateVPC(vpcOptions)
if err != nil {
return nil, fmt.Errorf("error creating vpc: %w", err)
} else if vpcDetails == nil {
return nil, fmt.Errorf("no vpc details after creation")
}
if err = s.TagResource(s.IBMVPCCluster.Name, *vpcDetails.CRN); err != nil {
return nil, fmt.Errorf("error tagging vpc: %w", err)
}

return vpcDetails, nil
}
Loading