Skip to content

Commit

Permalink
feat: add VnicAttachments reconciler (#205)
Browse files Browse the repository at this point in the history
This allows users to define vnic attachments. The reconciler
will then attach them after the instance is launched.
  • Loading branch information
joekr authored Feb 3, 2023
1 parent 942e650 commit 95aa2c6
Show file tree
Hide file tree
Showing 17 changed files with 557 additions and 14 deletions.
2 changes: 1 addition & 1 deletion Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ if "default_registry" in settings:

tilt_helper_dockerfile_header = """
# Tilt image
FROM golang:1.17 as tilt-helper
FROM golang:1.18 as tilt-helper
# Support live reloading with Tilt
RUN wget --output-document /restart.sh --quiet https://raw.githubusercontent.com/windmilleng/rerun-process-wrapper/master/restart.sh && \
wget --output-document /start.sh --quiet https://raw.githubusercontent.com/windmilleng/rerun-process-wrapper/master/start.sh && \
Expand Down
4 changes: 4 additions & 0 deletions api/v1beta1/conditions_consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ const (
FailureDomainFailedReason = "FailureDomainFailedReconciliationFailed"
// InstanceLBBackendAdditionFailedReason used when addition to LB backend fails
InstanceLBBackendAdditionFailedReason = "BackendAdditionFailed"
// InstanceVnicAttachmentFailedReason used when attaching vnics to machine
InstanceVnicAttachmentFailedReason = "VnicAttachmentFailed"
// InstanceIPAddressNotFound used when IP address of the instance count not be found
InstanceIPAddressNotFound = "InstanceIPAddressNotFound"
// VcnEventReady used after reconciliation has completed successfully
Expand All @@ -91,6 +93,8 @@ const (
RouteTableEventReady = "RouteTableReady"
// SubnetEventReady used after reconciliation has completed successfully
SubnetEventReady = "SubnetReady"
// InstanceVnicAttachmentReady used after reconciliation has been completed successfully
InstanceVnicAttachmentReady = "VnicAttachmentReady"
// ApiServerLoadBalancerEventReady used after reconciliation has completed successfully
ApiServerLoadBalancerEventReady = "APIServerLoadBalancerReady"
// FailureDomainEventReady used after reconciliation has completed successfully
Expand Down
4 changes: 4 additions & 0 deletions api/v1beta1/ocimachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ type OCIMachineSpec struct {
// NetworkDetails defines the configuration options for the network
NetworkDetails NetworkDetails `json:"networkDetails,omitempty"`

// VnicAttachments defines the configuration options for the vnic(s) attached to the machine
// The network bandwidth and number of VNICs scale proportionately with the number of OCPUs.
VnicAttachments []VnicAttachment `json:"vnicAttachments,omitempty"`

// LaunchOptions defines the options for tuning the compatibility and performance of VM shapes
LaunchOptions *LaunchOptions `json:"launchOptions,omitempty"`

Expand Down
24 changes: 24 additions & 0 deletions api/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,30 @@ type NetworkDetails struct {
AssignPrivateDnsRecord *bool `json:"assignPrivateDnsRecord,omitempty"`
}

type VnicAttachment struct {
// VnicAttachmentId defines the ID of the VnicAttachment
VnicAttachmentId *string `json:"vnicAttachmentId,omitempty"`

// AssignPublicIp defines whether the vnic should have a public IP address
// +optional
AssignPublicIp bool `json:"assignPublicIp,omitempty"`

// SubnetName defines the subnet name to use for the VNIC
// Defaults to the "worker" subnet if not provided
// +optional
SubnetName string `json:"subnetName,omitempty"`

// DisplayName defines a user-friendly name. Does not have to be unique.
// Avoid entering confidential information.
DisplayName *string `json:"displayName"`

// NicIndex defines which physical Network Interface Card (NIC) to use
// You can determine which NICs are active for a shape by reviewing the
// https://docs.oracle.com/en-us/iaas/Content/Compute/References/computeshapes.htm
// +optional
NicIndex *int `json:"nicIndex,omitempty"`
}

// LaunchOptionsBootVolumeTypeEnum Enum with underlying type: string
type LaunchOptionsBootVolumeTypeEnum string

Expand Down
37 changes: 37 additions & 0 deletions api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 17 additions & 13 deletions cloud/scope/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,16 +220,7 @@ func (m *MachineScope) GetOrCreateMachine(ctx context.Context) (*core.Instance,

tags := m.getFreeFormTags(*m.OCICluster)

definedTags := make(map[string]map[string]interface{})
if m.OCIMachine.Spec.DefinedTags != nil {
for ns, mapNs := range m.OCIMachine.Spec.DefinedTags {
mapValues := make(map[string]interface{})
for k, v := range mapNs {
mapValues[k] = v
}
definedTags[ns] = mapValues
}
}
definedTags := ConvertMachineDefinedTags(m.OCIMachine.Spec.DefinedTags)

availabilityDomain := m.OCICluster.Status.FailureDomains[*failureDomain].Attributes[AvailabilityDomain]
faultDomain := m.OCICluster.Status.FailureDomains[*failureDomain].Attributes[FaultDomain]
Expand Down Expand Up @@ -309,8 +300,8 @@ func (m *MachineScope) DeleteMachine(ctx context.Context) error {

// IsResourceCreatedByClusterAPI determines if the instance was created by the cluster using the
// tags created at instance launch.
func (s *MachineScope) IsResourceCreatedByClusterAPI(resourceFreeFormTags map[string]string) bool {
tagsAddedByClusterAPI := ociutil.BuildClusterTags(string(s.OCICluster.GetOCIResourceIdentifier()))
func (m *MachineScope) IsResourceCreatedByClusterAPI(resourceFreeFormTags map[string]string) bool {
tagsAddedByClusterAPI := ociutil.BuildClusterTags(string(m.OCICluster.GetOCIResourceIdentifier()))
for k, v := range tagsAddedByClusterAPI {
if resourceFreeFormTags[k] != v {
return false
Expand Down Expand Up @@ -474,8 +465,10 @@ func (m *MachineScope) GetInstanceIp(ctx context.Context) (*string, error) {
}
}

if page = resp.OpcNextPage; resp.OpcNextPage == nil {
if resp.OpcNextPage == nil {
break
} else {
page = resp.OpcNextPage
}
}

Expand Down Expand Up @@ -620,6 +613,17 @@ func (m *MachineScope) getGetControlPlaneMachineNSGs() []string {
return nsgs
}

// getMachineSubnet iterates through the OCICluster Vcn subnets
// and returns the subnet ID if the name matches
func (m *MachineScope) getMachineSubnet(name string) (*string, error) {
for _, subnet := range m.OCICluster.Spec.NetworkSpec.Vcn.Subnets {
if subnet.Name == name {
return subnet.ID, nil
}
}
return nil, errors.New(fmt.Sprintf("Subnet with name %s not found for cluster %s", name, m.OCICluster.Name))
}

func (m *MachineScope) getWorkerMachineSubnet() *string {
for _, subnet := range m.OCICluster.Spec.NetworkSpec.Vcn.Subnets {
if subnet.Role == infrastructurev1beta1.WorkerRole {
Expand Down
17 changes: 17 additions & 0 deletions cloud/scope/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,20 @@ func GetSubnetNamesFromId(ids []string, subnets []*infrastructurev1beta1.Subnet)
}
return names
}

// ConvertMachineDefinedTags passes in the OCIMachineSpec DefinedTags and returns a converted map of defined tags
// to be used when creating API requests.
func ConvertMachineDefinedTags(machineDefinedTags map[string]map[string]string) map[string]map[string]interface{} {
definedTags := make(map[string]map[string]interface{})
if machineDefinedTags != nil {
for ns, mapNs := range machineDefinedTags {
mapValues := make(map[string]interface{})
for k, v := range mapNs {
mapValues[k] = v
}
definedTags[ns] = mapValues
}
}

return definedTags
}
129 changes: 129 additions & 0 deletions cloud/scope/vnic_reconciler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
Copyright (c) 2022 Oracle and/or its affiliates.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package scope

import (
"context"
"fmt"
"github.com/oracle/cluster-api-provider-oci/cloud/ociutil"

infrastructurev1beta1 "github.com/oracle/cluster-api-provider-oci/api/v1beta1"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/oracle/oci-go-sdk/v65/core"
"github.com/pkg/errors"
)

func (m *MachineScope) ReconcileVnicAttachments(ctx context.Context) error {
if m.IsControlPlane() {
return errors.New("cannot attach multiple vnics to ControlPlane machines")
}

for index, vnicAttachment := range m.OCIMachine.Spec.VnicAttachments {
if m.vnicAttachmentExists(ctx, vnicAttachment) {
m.Logger.Info("vnicAttachment", ociutil.DerefString(vnicAttachment.DisplayName), " already exists and is immutable")
continue
}

vnicId, err := m.createVnicAttachment(ctx, vnicAttachment)
if err != nil {
msg := fmt.Sprintf("Error creating VnicAttachment %s for cluster %s",
*vnicAttachment.DisplayName, m.Cluster.Name)
m.Logger.Error(err, msg)
return err
}

m.OCIMachine.Spec.VnicAttachments[index].VnicAttachmentId = vnicId
}

return nil
}

func (m *MachineScope) createVnicAttachment(ctx context.Context, spec infrastructurev1beta1.VnicAttachment) (*string, error) {
vnicName := spec.DisplayName

// Default to machine subnet if spec doesn't supply one
subnetId := m.getWorkerMachineSubnet()
if spec.SubnetName != "" {
var err error
subnetId, err = m.getMachineSubnet(spec.SubnetName)
if err != nil {
return nil, err
}
}

tags := m.getFreeFormTags(*m.OCICluster)

definedTags := ConvertMachineDefinedTags(m.OCIMachine.Spec.DefinedTags)

if spec.NicIndex == nil {
spec.NicIndex = common.Int(0)
}

secondVnic := core.AttachVnicDetails{
DisplayName: vnicName,
NicIndex: spec.NicIndex,
InstanceId: m.OCIMachine.Spec.InstanceId,
CreateVnicDetails: &core.CreateVnicDetails{
SubnetId: subnetId,
AssignPublicIp: common.Bool(spec.AssignPublicIp),
FreeformTags: tags,
DefinedTags: definedTags,
HostnameLabel: m.OCIMachine.Spec.NetworkDetails.HostnameLabel,
NsgIds: m.getWorkerMachineNSGs(),
SkipSourceDestCheck: m.OCIMachine.Spec.NetworkDetails.SkipSourceDestCheck,
AssignPrivateDnsRecord: m.OCIMachine.Spec.NetworkDetails.AssignPrivateDnsRecord,
DisplayName: vnicName,
},
}

req := core.AttachVnicRequest{AttachVnicDetails: secondVnic}
resp, err := m.ComputeClient.AttachVnic(ctx, req)
if err != nil {
return nil, err
}

return resp.Id, nil
}

func (m *MachineScope) vnicAttachmentExists(ctx context.Context, vnic infrastructurev1beta1.VnicAttachment) bool {

found := false
var page *string
for {
resp, err := m.ComputeClient.ListVnicAttachments(ctx, core.ListVnicAttachmentsRequest{
InstanceId: m.GetInstanceId(),
CompartmentId: common.String(m.getCompartmentId()),
Page: page,
})
if err != nil {
return false
}
for _, attachment := range resp.Items {
if ociutil.DerefString(attachment.DisplayName) == ociutil.DerefString(vnic.DisplayName) {
m.Logger.Info("Vnic is already attached ", attachment)
return true
}
}

if resp.OpcNextPage == nil {
break
} else {
page = resp.OpcNextPage
}
}
return found
}
Loading

0 comments on commit 95aa2c6

Please sign in to comment.