diff --git a/README.md b/README.md index 4c87bc4..a3fad49 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Welcome to KubeIP v2, a complete overhaul of the popular [DoiT](https://www.doit KubeIP [v1-main](https://github.com/doitintl/kubeip/tree/v1-main) open-source project, originally developed by [Aviv Laufer](https://github.com/avivl). -KubeIP v2 expands its support beyond Google Cloud (as in v1) to include AWS, and it's designed to be extendable to other cloud providers +KubeIP v2 expands its support beyond Google Cloud (as in v1) to include AWS and Oracle Cloud Infrastructure(OCI), and it's designed to be extendable to other cloud providers that allow assigning static public IP to VMs. We've also transitioned from a Kubernetes controller to a standard DaemonSet, enhancing reliability and ease of use. @@ -252,6 +252,93 @@ To use this feature, add the `filter` flag (or set `FILTER` environment variable value: "labels.env=dev;labels.app=streamer" ``` +### Oracle Cloud Infrastructure (OCI) + +Make sure that KubeIP DaemonSet is deployed on nodes that have a public IP (node running in public subnet). Set the [compartment OCID](https://docs.oracle.com/en-us/iaas/Content/GSG/Tasks/contactingsupport_topic-Locating_Oracle_Cloud_Infrastructure_IDs.htm#Finding_the_OCID_of_a_Compartment) in the `project` flag (or +set `FILTER` environment variable) to the KubeIP DaemonSet: + +```yaml +- name: PROJECT + value: "ocid1.compartment.oc1..test" +``` + +KubeIP will also need certain permissions to communicate with the OCI APIs. Follow these steps to set up the necessary permissions and generate the required API key and place it in the KubeIP DaemonSet: + +1. Create a [user and group](https://docs.oracle.com/en/cloud/paas/integration-cloud/oracle-integration-gov/create-iam-group.html) in the OCI console and add the following [policy](https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingpolicies.htm) to the group: + + ``` + Allow group to manage public-ips in compartment id + Allow group to manage private-ips in compartment id + Allow group to manage vcns in compartment id + ``` + +2. Generate an [API Key](https://docs.oracle.com/en-us/iaas/Content/API/Concepts/apisigningkey.htm#two) for the user and download the private key. Config file will look like this: + + ``` + [DEFAULT] + user=ocid1.user.oc1..test + fingerprint= + key_file=/root/.oci/oci_api_key.pem + tenancy=ocid1.tenancy.oc1..test + region=us-ashburn-1 + ``` + +3. Add the following [secret](https://kubernetes.io/docs/concepts/configuration/secret/) to the KubeIP DaemonSet: + + ```yaml + apiVersion: v1 + kind: Secret + metadata: + name: kubeip-oci-secret + namespace: kube-system + type: Opaque + data: + config: + oci_api_key.pem: + ``` + +4. Create a volume and mount in the KubeIP DaemonSet to mount the secret: + + ```yaml + volumes: + - name: oci-config + secret: + secretName: kubeip-oci-secret + ``` + + ```yaml + volumeMounts: + - name: oci-config + mountPath: /root/.oci + ``` + +5. Add the following environment variables to the KubeIP DaemonSet: + ```yaml + - name: OCI_CONFIG_FILE + value: /root/.oci/config + ``` + +KubeIP supports filtering of reserved Public IPs using tags. To use this feature, add the `filter` flag (or +set `FILTER` environment variable) to the KubeIP DaemonSet: + +```yaml +- name: FILTER + value: "freeformTags.env=dev" +``` + +KubeIP OCI filter supports the following filter syntax: + +- `freeformTags.=` + +To specify multiple filters, separate them with a semicolon (`;`). For example: + +```yaml +- name: FILTER + value: "freeformTags.env=dev;freeformTags.app=streamer" +``` + +In the case of multiple filters, they are joined with an `AND`, and the request returns only results that match all the specified filters. + ## How to contribute to KubeIP? KubeIP is an open-source project, and we welcome your contributions! @@ -287,8 +374,8 @@ OPTIONS: --kubeconfig value path to Kubernetes configuration file (not needed if running in node) [$KUBECONFIG] --node-name value Kubernetes node name (not needed if running in node) [$NODE_NAME] --order-by value order by for the IP addresses [$ORDER_BY] - --project value name of the GCP project or the AWS account ID (not needed if running in node) [$PROJECT] - --region value name of the GCP region or the AWS region (not needed if running in node) [$REGION] + --project value name of the GCP project or the AWS account ID (not needed if running in node) or OCI compartment OCID (required for OCI) [$PROJECT] + --region value name of the GCP region or the AWS region or the OCI region (not needed if running in node) [$REGION] --release-on-exit release the static public IP address on exit (default: true) [$RELEASE_ON_EXIT] --taint-key value specify a taint key to remove from the node once the static public IP address is assigned [$TAINT_KEY] --retry-attempts value number of attempts to assign the static public IP address (default: 10) [$RETRY_ATTEMPTS] diff --git a/chart/templates/daemonset.yaml b/chart/templates/daemonset.yaml index 9c1d59e..2341e21 100644 --- a/chart/templates/daemonset.yaml +++ b/chart/templates/daemonset.yaml @@ -40,6 +40,11 @@ spec: imagePullPolicy: Always resources: {{- toYaml .Values.daemonSet.resources | nindent 12 }} + {{- if eq .Values.cloudProvider "oci" }} + volumeMounts: + - name: oci-config + mountPath: /root/.oci + {{- end }} env: - name: NODE_NAME valueFrom: @@ -53,6 +58,10 @@ spec: value: {{ .Values.daemonSet.env.LOG_LEVEL | quote }} - name: LOG_JSON value: {{ .Values.daemonSet.env.LOG_JSON | quote }} + {{- if eq .Values.cloudProvider "oci" }} + - name: OCI_CONFIG_FILE + value: /root/.oci/config + {{- end }} securityContext: privileged: false allowPrivilegeEscalation: false @@ -60,3 +69,9 @@ spec: drop: - ALL readOnlyRootFilesystem: true + {{- if eq .Values.cloudProvider "oci" }} + volumes: + - name: oci-config + secret: + secretName: oci-config + {{- end }} diff --git a/chart/templates/secrets.yaml b/chart/templates/secrets.yaml new file mode 100644 index 0000000..aa1f908 --- /dev/null +++ b/chart/templates/secrets.yaml @@ -0,0 +1,11 @@ +{{- if .Values.secrets.create }} +apiVersion: v1 +kind: Secret +metadata: + name: kubeip-oci-secret + namespace: {{ .Values.namespaceOverride }} +type: Opaque +data: + oci_config: {{ .Values.secrets.oci_config }} + oci_oci_api_key.pem: {{ .Values.secrets.oci_oci_api_key }} +{{- end }} \ No newline at end of file diff --git a/chart/values.yaml b/chart/values.yaml index 9cdd362..5f09d99 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -1,6 +1,6 @@ # The cloud provider where your Kubernetes cluster is running. # This value determines the appropriate annotations for the Service Account. -# Currently acceptable values are 'gcp' or 'aws'. +# Currently acceptable values are 'gcp' or 'aws' or 'oci'. cloudProvider: gcp # The namespace where the kubeip-agent will be deployed. @@ -27,6 +27,12 @@ rbac: create: true allowNodesPatchPermission: false +# Secret configuration for oci users. +secrets: + create: true + oci_config: "" # base64 encoded oci config file + oci_oci_api_key: "" # base64 encoded oci api key file + # DaemonSet configuration. daemonSet: terminationGracePeriodSeconds: 30 diff --git a/cmd/main.go b/cmd/main.go index 0f9950a..c236cde 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -316,13 +316,13 @@ func main() { }, &cli.StringFlag{ Name: "project", - Usage: "name of the GCP project or the AWS account ID (not needed if running in node)", + Usage: "name of the GCP project or the AWS account ID (not needed if running in node) or OCI compartment OCID (required for OCI)", EnvVars: []string{"PROJECT"}, Category: "Configuration", }, &cli.StringFlag{ Name: "region", - Usage: "name of the GCP region or the AWS region (not needed if running in node)", + Usage: "name of the GCP region or the AWS region or the OCI region (not needed if running in node)", EnvVars: []string{"REGION"}, Category: "Configuration", }, diff --git a/go.mod b/go.mod index 69c1b51..46bb14f 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/aws/aws-sdk-go-v2 v1.26.0 github.com/aws/aws-sdk-go-v2/config v1.27.9 github.com/aws/aws-sdk-go-v2/service/ec2 v1.152.0 + github.com/oracle/oci-go-sdk/v65 v65.80.0 github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 @@ -15,6 +16,7 @@ require ( k8s.io/api v0.29.3 k8s.io/apimachinery v0.29.3 k8s.io/client-go v0.29.3 + k8s.io/utils v0.0.0-20240310230437-4693a0247e57 sigs.k8s.io/controller-runtime v0.17.2 ) @@ -41,6 +43,7 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/gofrs/flock v0.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect @@ -60,6 +63,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sony/gobreaker v0.5.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect @@ -85,7 +89,6 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940 // indirect - k8s.io/utils v0.0.0-20240310230437-4693a0247e57 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index 78fce22..695b14b 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -138,6 +140,8 @@ github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/oracle/oci-go-sdk/v65 v65.80.0 h1:Rr7QLMozd2DfDBKo6AB3DzLYQxAwuOG118+K5AAD5E8= +github.com/oracle/oci-go-sdk/v65 v65.80.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -149,6 +153,8 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sony/gobreaker v0.5.0 h1:dRCvqm0P490vZPmy7ppEk2qCnCieBooFJ+YoXGYB+yg= +github.com/sony/gobreaker v0.5.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -161,6 +167,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho= @@ -226,6 +233,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/internal/address/assigner.go b/internal/address/assigner.go index 773111d..cea35a8 100644 --- a/internal/address/assigner.go +++ b/internal/address/assigner.go @@ -27,6 +27,8 @@ func NewAssigner(ctx context.Context, logger *logrus.Entry, provider types.Cloud return &azureAssigner{}, nil } else if provider == types.CloudProviderGCP { return NewGCPAssigner(ctx, logger, cfg.Project, cfg.Region, cfg.IPv6) + } else if provider == types.CloudProviderOCI { + return NewOCIAssigner(ctx, logger, cfg) } return nil, ErrUnknownCloudProvider } diff --git a/internal/address/aws.go b/internal/address/aws.go index 0e4098d..5bbd007 100644 --- a/internal/address/aws.go +++ b/internal/address/aws.go @@ -236,7 +236,7 @@ func (a *awsAssigner) tryAssignAddress(ctx context.Context, address *types.Addre func (a *awsAssigner) getNetworkInterfaceID(instance *types.Instance) (string, error) { // get network interface ID - if instance.NetworkInterfaces == nil || len(instance.NetworkInterfaces) == 0 { + if len(instance.NetworkInterfaces) == 0 { return "", errors.Errorf("no network interfaces found for instance %s", *instance.InstanceId) } // get primary network interface ID with public IP address (DeviceIndex == 0) diff --git a/internal/address/oci.go b/internal/address/oci.go new file mode 100644 index 0000000..72373fe --- /dev/null +++ b/internal/address/oci.go @@ -0,0 +1,345 @@ +package address + +import ( + "context" + "strings" + + "github.com/doitintl/kubeip/internal/cloud" + "github.com/doitintl/kubeip/internal/config" + "github.com/doitintl/kubeip/internal/types" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/core" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// ociAssigner is an Assigner implementation for Oracle Cloud Infrastructure. +type ociAssigner struct { + logger *logrus.Entry + filters *types.OCIFilters + compartmentOCID string + instanceSvc cloud.OCIInstanceService + networkSvc cloud.OCINetworkService +} + +// NewOCIAssigner creates a new Assigner for Oracle Cloud Infrastructure. +func NewOCIAssigner(_ context.Context, logger *logrus.Entry, cfg *config.Config) (Assigner, error) { + logger.WithFields( + logrus.Fields{ + "compartmentOCID": cfg.Project, + "filters": cfg.Filter, + }, + ).Info("creating new OCI assigner with given config") + + // Parse the filters + filters, err := parseOCIFilters(cfg) + if err != nil { + return nil, errors.Wrap(err, "failed to parse OCI filters") + } + if filters == nil { + logger.Warn("no filters provided, any ip from the list of all public IPs present in the project can be used") + } + + // Create a new instance svc + computeSvc, err := cloud.NewOCIInstanceService() + if err != nil { + return nil, errors.Wrap(err, "failed to create compute service for OCI") + } + + // Create a new network svc + networkSvc, err := cloud.NewOCINetworkService() + if err != nil { + return nil, errors.Wrap(err, "failed to create network service for OCI") + } + + return &ociAssigner{ + logger: logger, + filters: filters, + instanceSvc: computeSvc, + networkSvc: networkSvc, + compartmentOCID: cfg.Project, + }, nil +} + +// Assign assigns reserved Public IP to the instance. +// If the instance already has a public IP assigned, and it is from the reserved list, it returns the same IP. +// Else it assigns a new public IP from the reserved list. +func (a *ociAssigner) Assign(ctx context.Context, instanceOCID, _ string, _ []string, _ string) (string, error) { + a.logger.WithField("instanceOCID", instanceOCID).Debug("starting process to assign reserved public IP to instance") + + // Get the primary VNIC + vnic, err := a.getPrimaryVnicOfInstance(ctx, instanceOCID) + if err != nil { + return "", err + } + a.logger.WithField("primaryVnicOCID", *vnic.Id).Debugf("got primary VNIC of the instance %s", instanceOCID) + + // Handle already assigned public IP case + alreadyAssigned, err := a.handlePublicIPAlreadyAssignedCase(ctx, vnic) + if err != nil { + return "", errors.Wrap(err, "failed to check if public ip is already assigned or not") + } + if alreadyAssigned { + a.logger.WithField("alreadyAssignedIP", *vnic.PublicIp).Infof("reserved public IP already assigned on instance %s", instanceOCID) + return *vnic.PublicIp, ErrStaticIPAlreadyAssigned + } + + // Get primary VNIC private IP + privateIP, err := a.networkSvc.GetPrimaryPrivateIPOfVnic(ctx, *vnic.Id) + if err != nil { + return "", errors.Wrap(err, "failed to get primary VNIC private IP") + } + a.logger.WithField("privateIPOCID", *privateIP.Id).Debugf("got primary VNIC private IP of the instance %s", instanceOCID) + + // Fetch all available reserved Public IPs that will be used for assignment + reservedPublicIPList, err := a.fetchPublicIps(ctx, true, false) + if err != nil { + return "", errors.Wrap(err, "failed to get list of reserved public IPs") + } + if len(reservedPublicIPList) == 0 { + return "", errors.New("no reserved public IPs available") + } + a.logger.WithField("reservedPublicIpList", reservedPublicIPList).Debug("got list of available reserved public IPs") + + // Try to assign an IP from the reserved public IP list + for _, publicIP := range reservedPublicIPList { + if err = a.tryAssignAddress(ctx, *privateIP.Id, *publicIP.Id); err == nil { + a.logger.WithField("assignedIP", *publicIP.IpAddress).Infof("assigned IP %s to instance %s", *publicIP.IpAddress, instanceOCID) + return *publicIP.IpAddress, nil + } + a.logger.Warnf("Failed to assign IP %s to instance %s: %v", *publicIP.IpAddress, instanceOCID, err) + } + + return "", errors.New("failed to assign any IP") +} + +// Unassign unassigns the public IP from the instance. +// If assigned public IP is from the reserved public IP list, it unassigns the public IP. +// Else it does nothing. +func (a *ociAssigner) Unassign(ctx context.Context, instanceOCID, _ string) error { + a.logger.WithField("instanceOCID", instanceOCID).Debug("starting process to unassign public IP from the instance") + + // Get the primary VNIC + vnic, err := a.getPrimaryVnicOfInstance(ctx, instanceOCID) + if err != nil { + return err + } + + // If no public IP is assigned, return + if vnic.PublicIp == nil { + a.logger.Infof("no public ip assigned to the instance %s", instanceOCID) + return ErrNoPublicIPAssigned + } + publicIP := vnic.PublicIp + + // Fetch assigned public IPs + reservedPublicIPList, err := a.fetchPublicIps(ctx, true, true) + if err != nil { + return errors.Wrap(err, "failed to get list of reserved public IPs") + } + a.logger.WithField("reservedPublicIPList", reservedPublicIPList).Debug("got list of reserved public IPs") + + // Check if assigned public ip is from the reserved public IP list + for _, ip := range reservedPublicIPList { + if *ip.IpAddress == *publicIP && ip.LifecycleState == core.PublicIpLifecycleStateAssigned { + // Unassign the public IP + if err := a.networkSvc.UpdatePublicIP(ctx, *ip.Id, ""); err != nil { + return errors.Wrap(err, "failed to unassign public IP assigned to private IP") + } + return nil + } + } + + return errors.New("public IP not assigned from reserved list") +} + +// getPrimaryVnicOfInstance returns the primary VNIC of the instance from the VNIC attachment. +func (a *ociAssigner) getPrimaryVnicOfInstance(ctx context.Context, instanceOCID string) (*core.Vnic, error) { + // Get VNIC attachment of the instance + vnicAttachment, err := a.instanceSvc.ListVnicAttachments(ctx, a.compartmentOCID, instanceOCID) + if err != nil { + return nil, errors.Wrap(err, "failed to list VNIC attachments") + } + + // Get the primary VNIC + vnic, err := a.networkSvc.GetPrimaryVnic(ctx, vnicAttachment) + if err != nil { + return nil, errors.Wrap(err, "failed to get primary VNIC") + } + + return vnic, nil +} + +// handlePublicIPAlreadyAssignedCase handles the case when the public IP is already assigned to the instance. +// It returns true if the public IP is already assigned to the instance from the reserved IP list. In this case, do nothing. +// It returns false in all other cases with error(if any). In this case, if err is nil, try to assign a new public IP. +// Following are the cases and actions for each case: +// - Case1: Public IP is already assigned to the instance from the reserved IP list: Do nothing +// - Case2: Public IP is assigned to the instance but not from the reserved IP list: Unassign the public IP +// - Case3: Public IP is assigned to the instance, but it is ephemeral: Delete the ephemeral public IP +// - Case4: Unhandled case: Return error +// +//nolint:gocognit +func (a *ociAssigner) handlePublicIPAlreadyAssignedCase(ctx context.Context, vnic *core.Vnic) (bool, error) { + if vnic == nil { + return false, nil + } + publicIP := vnic.PublicIp + if publicIP != nil { + // Case1 + // Fetch all reserved public IPs that are assigned to the private IPs + list, err := a.fetchPublicIps(ctx, true, true) + if err != nil { + return false, errors.Wrap(err, "failed to list reserved public IPs assigned to private IP") + } + for _, ip := range list { + if *ip.IpAddress == *publicIP { + return true, nil + } + } + + // Case2 + // Fetch all public IPs that are assigned to the private IPs + list, err = a.fetchPublicIps(ctx, false, true) + if err != nil { + return false, errors.Wrap(err, "failed to list public IPs assigned to private IP") + } + for _, ip := range list { + if *ip.IpAddress == *publicIP { + // Unassign the public IP + if err = a.networkSvc.UpdatePublicIP(ctx, *ip.Id, ""); err != nil { + return false, errors.Wrap(err, "failed to unassign public IP assigned to private IP") + } + return false, nil + } + } + + // Case3 + // Fetch ephemeral public IPs assigned to private IPs + if vnic.AvailabilityDomain == nil { + return false, errors.New("availability domain not found") + } + list, err = a.fetchEphemeralPublicIPs(ctx, *vnic.AvailabilityDomain) + if err != nil { + return false, errors.Wrap(err, "failed to list ephemeral public IPs assigned to private IP") + } + for _, ip := range list { + if *ip.IpAddress == *publicIP { + // Delete the ephemeral public IP + if err := a.networkSvc.DeletePublicIP(ctx, *ip.Id); err != nil { + return false, errors.Wrap(err, "failed to delete ephemeral public IP assigned to private IP") + } + return false, nil + } + } + + // Case4 + // Unhandled case + return false, errors.New("unhandled case: public IP is assigned to the instance but not from the reserved IP list") + } + + return false, nil +} + +// fetchPublicIps returns the list of public IPs. +// If useFilter is set to true, it applies the filters. +// It returns only available public IPs if inUse is set to false. +// It returns only assigned public IPs if inUse is set to true. +func (a *ociAssigner) fetchPublicIps(ctx context.Context, useFilter, inUse bool) ([]core.PublicIp, error) { + filters := a.filters + // If useFilter is set to false, do not apply the filters + if !useFilter { + filters = nil + } + list, err := a.networkSvc.ListPublicIps(ctx, &core.ListPublicIpsRequest{ + Scope: core.ListPublicIpsScopeRegion, + CompartmentId: common.String(a.compartmentOCID), + }, filters) + if err != nil { + return nil, errors.Wrap(err, "failed to list public IPs") + } + + lifecycleState := core.PublicIpLifecycleStateAvailable + // If inUse is set to true, only return assigned public IPs + if inUse { + lifecycleState = core.PublicIpLifecycleStateAssigned + } + + // Return IPs that match the given lifecycleState. + var updatedList []core.PublicIp + for _, ip := range list { + if ip.LifecycleState == lifecycleState { + updatedList = append(updatedList, ip) + } + } + return updatedList, nil +} + +// fetchEphemeralPublicIPs returns the list of ephemeral public IPs assigned to the private IPs in the availability domain. +func (a *ociAssigner) fetchEphemeralPublicIPs(ctx context.Context, availabilityDomain string) ([]core.PublicIp, error) { + list, err := a.networkSvc.ListPublicIps(ctx, &core.ListPublicIpsRequest{ + Scope: core.ListPublicIpsScopeAvailabilityDomain, + AvailabilityDomain: common.String(availabilityDomain), + CompartmentId: common.String(a.compartmentOCID), + }, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to list ephemeral public IPs") + } + + return list, nil +} + +// tryAssignAddress tries to assign the public IP to the private IP. +// If the public IP is not available, it returns an error. +func (a *ociAssigner) tryAssignAddress(ctx context.Context, privateIPOCID, publicIPOCID string) error { + // Fetch public IP details to check if it is available + publicIP, err := a.networkSvc.GetPublicIP(ctx, publicIPOCID) + if err != nil { + return errors.Wrap(err, "failed to get public IP details") + } + if publicIP == nil { + return errors.New("public IP not found") + } + + // If public IP is not available, return + if publicIP.LifecycleState != core.PublicIpLifecycleStateAvailable { + return errors.New("public IP is not available") + } + + // Assign the public IP to the private IP + if err := a.networkSvc.UpdatePublicIP(ctx, *publicIP.Id, privateIPOCID); err != nil { + return errors.Wrap(err, "failed to assign public IP") + } + + return nil +} + +// ParseOCIFilters parses the filters for OCI from the config. +// All filters of freeformTags are combined with AND condition. +// All filters of definedTags are combined with AND condition. +// Filter should be in following format: +// - "freeformTags.key1=value1" +// - "definedTags.Namespace.key1=value1" +func parseOCIFilters(cfg *config.Config) (*types.OCIFilters, error) { + if cfg == nil { + return nil, errors.New("config is nil") + } + + freeformTags := make(map[string]string) + + for _, filter := range cfg.Filter { + if strings.HasPrefix(filter, "freeformTags.") { + key, value, err := types.ParseFreeformTagFilter(filter) + if err != nil { + return nil, errors.Wrap(err, "failed to parse freeform tag filter") + } + freeformTags[key] = value + } else { + return nil, errors.New("invalid filter format for OCI, should be in format freeformTags.key=value or definedTags.Namespace.key=value, found: " + filter) + } + } + + return &types.OCIFilters{ + FreeformTags: freeformTags, + }, nil +} diff --git a/internal/address/oci_test.go b/internal/address/oci_test.go new file mode 100644 index 0000000..c5c9133 --- /dev/null +++ b/internal/address/oci_test.go @@ -0,0 +1,1261 @@ +package address + +import ( + "context" + "reflect" + "testing" + + "github.com/doitintl/kubeip/internal/cloud" + "github.com/doitintl/kubeip/internal/config" + "github.com/doitintl/kubeip/internal/types" + cmocks "github.com/doitintl/kubeip/mocks/cloud" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/core" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/mock" +) + +func matchErr(err1 error, err2 error) bool { + errStr1 := "" + errStr2 := "" + if err1 != nil { + errStr1 = err1.Error() + } + if err2 != nil { + errStr2 = err2.Error() + } + return errStr1 == errStr2 +} + +func Test_ociAssigner_Assign(t *testing.T) { + type args struct { + compartmentOCID string + instanceOCID string + filters *types.OCIFilters + } + type fields struct { + logger *logrus.Entry + instanceSvcFn func(t *testing.T, args *args) cloud.OCIInstanceService + networkSvcFn func(t *testing.T, args *args) cloud.OCINetworkService + } + tests := []struct { + name string + fields fields + args args + want string + wantErr error + }{ + { + name: "assign reserved public IP", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(&core.Vnic{ + Id: common.String("test-vnic-id"), + }, nil).Once() + mockSvc.EXPECT().GetPrimaryPrivateIPOfVnic(mock.Anything, mock.Anything).Return(&core.PrivateIp{ + Id: common.String("test-private-ip-id"), + }, nil).Once() + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{ + {Id: common.String("test-public-ip"), IpAddress: common.String("1.2.3.4"), LifecycleState: core.PublicIpLifecycleStateAvailable}, + }, nil).Once() + mockSvc.EXPECT().GetPublicIP(mock.Anything, mock.Anything).Return(&core.PublicIp{ + Id: common.String("test-public-ip"), IpAddress: common.String("1.2.3.4"), LifecycleState: core.PublicIpLifecycleStateAvailable, + }, nil).Once() + mockSvc.EXPECT().UpdatePublicIP(mock.Anything, mock.Anything, mock.Anything).Return(nil).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + want: "1.2.3.4", + }, + { + name: "failed to get primary VNIC of instance", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{}, errors.New("error")).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + return cmocks.NewOCINetworkService(t) + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + wantErr: errors.New("failed to list VNIC attachments: error"), + }, + { + name: "failed to check public IP already assigned", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(&core.Vnic{ + Id: common.String("test-vnic-id"), + PublicIp: common.String("1.2.3.4"), + }, nil).Once() + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("error")).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + wantErr: errors.New("failed to check if public ip is already assigned or not: failed to list reserved public IPs assigned to private IP: failed to list public IPs: error"), + }, + { + name: "public IP already assigned from reserved list", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(&core.Vnic{ + Id: common.String("test-vnic-id"), + PublicIp: common.String("1.2.3.4"), + }, nil).Once() + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{ + {Id: common.String("test-public-ip"), IpAddress: common.String("1.2.3.4"), LifecycleState: core.PublicIpLifecycleStateAssigned}, + }, nil).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + want: "1.2.3.4", + wantErr: ErrStaticIPAlreadyAssigned, + }, + { + name: "failed to get primary private IP of VNIC", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(&core.Vnic{ + Id: common.String("test-vnic-id"), + }, nil).Once() + mockSvc.EXPECT().GetPrimaryPrivateIPOfVnic(mock.Anything, mock.Anything).Return(nil, errors.New("error")).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + wantErr: errors.New("failed to get primary VNIC private IP: error"), + }, + { + name: "failed to fetch reserved public IP list", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(&core.Vnic{ + Id: common.String("test-vnic-id"), + }, nil).Once() + mockSvc.EXPECT().GetPrimaryPrivateIPOfVnic(mock.Anything, mock.Anything).Return(&core.PrivateIp{ + Id: common.String("test-private-ip-id"), + }, nil).Once() + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("error")).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + wantErr: errors.New("failed to get list of reserved public IPs: failed to list public IPs: error"), + }, + { + name: "no reserved public IP available to assign", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(&core.Vnic{ + Id: common.String("test-vnic-id"), + }, nil).Once() + mockSvc.EXPECT().GetPrimaryPrivateIPOfVnic(mock.Anything, mock.Anything).Return(&core.PrivateIp{ + Id: common.String("test-private-ip-id"), + }, nil).Once() + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{}, nil).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + wantErr: errors.New("no reserved public IPs available"), + }, + { + name: "failed to assign public IP to private IP", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(&core.Vnic{ + Id: common.String("test-vnic-id"), + }, nil).Once() + mockSvc.EXPECT().GetPrimaryPrivateIPOfVnic(mock.Anything, mock.Anything).Return(&core.PrivateIp{ + Id: common.String("test-private-ip-id"), + }, nil).Once() + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{ + {Id: common.String("test-public-ip"), IpAddress: common.String("1.2.3.4"), LifecycleState: core.PublicIpLifecycleStateAvailable}, + }, nil).Once() + mockSvc.EXPECT().GetPublicIP(mock.Anything, mock.Anything).Return(&core.PublicIp{ + Id: common.String("test-public-ip"), IpAddress: common.String("1.2.3.4"), LifecycleState: core.PublicIpLifecycleStateAvailable, + }, nil).Once() + mockSvc.EXPECT().UpdatePublicIP(mock.Anything, mock.Anything, mock.Anything).Return(errors.New("error")).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + wantErr: errors.New("failed to assign any IP"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &ociAssigner{ + logger: tt.fields.logger, + filters: tt.args.filters, + compartmentOCID: tt.args.compartmentOCID, + instanceSvc: tt.fields.instanceSvcFn(t, &tt.args), + networkSvc: tt.fields.networkSvcFn(t, &tt.args), + } + got, err := a.Assign(context.TODO(), tt.args.instanceOCID, "", nil, "") + if !matchErr(err, tt.wantErr) { + t.Errorf("OCI Assign() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Fatalf("OCI Assign() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ociAssigner_Unassign(t *testing.T) { + type args struct { + compartmentOCID string + instanceOCID string + } + type fields struct { + logger *logrus.Entry + instanceSvcFn func(t *testing.T, args *args) cloud.OCIInstanceService + networkSvcFn func(t *testing.T, args *args) cloud.OCINetworkService + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "unassign public IP", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(&core.Vnic{ + Id: common.String("test-vnic-id"), + PublicIp: common.String("1.2.3.4"), + }, nil).Once() + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{ + {Id: common.String("test-public-ip"), IpAddress: common.String("1.2.3.4"), LifecycleState: core.PublicIpLifecycleStateAssigned}, + }, nil).Once() + mockSvc.EXPECT().UpdatePublicIP(mock.Anything, "test-public-ip", "").Return(nil).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + }, + { + name: "public ip not assigned from reserved list", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(&core.Vnic{ + Id: common.String("test-vnic-id"), + PublicIp: common.String("1.2.3.4"), + }, nil).Once() + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{}, nil).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + wantErr: errors.New("public IP not assigned from reserved list"), + }, + { + name: "failed to get primary VNIC of instance", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{}, errors.New("error")).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + return cmocks.NewOCINetworkService(t) + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + wantErr: errors.New("failed to list VNIC attachments: error"), + }, + { + name: "no public IP assigned", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(&core.Vnic{ + Id: common.String("test-vnic-id"), + }, nil).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + wantErr: ErrNoPublicIPAssigned, + }, + { + name: "failed to fetch assigned public IP", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(&core.Vnic{ + Id: common.String("test-vnic-id"), + PublicIp: common.String("1.2.3.4"), + }, nil).Once() + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{}, errors.New("error")).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + wantErr: errors.New("failed to get list of reserved public IPs: failed to list public IPs: error"), + }, + { + name: "failed to update public IP", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(&core.Vnic{ + Id: common.String("test-vnic-id"), + PublicIp: common.String("1.2.3.4"), + }, nil).Once() + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{ + {Id: common.String("test-public-ip"), IpAddress: common.String("1.2.3.4"), LifecycleState: core.PublicIpLifecycleStateAssigned}, + }, nil).Once() + mockSvc.EXPECT().UpdatePublicIP(mock.Anything, "test-public-ip", "").Return(errors.New("error")).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + wantErr: errors.New("failed to unassign public IP assigned to private IP: error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &ociAssigner{ + logger: tt.fields.logger, + instanceSvc: tt.fields.instanceSvcFn(t, &tt.args), + networkSvc: tt.fields.networkSvcFn(t, &tt.args), + compartmentOCID: tt.args.compartmentOCID, + } + err := a.Unassign(context.TODO(), tt.args.instanceOCID, "") + if !matchErr(err, tt.wantErr) { + t.Errorf("Unassign() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_ociAssigner_getPrimaryVnicOfInstance(t *testing.T) { + type args struct { + compartmentOCID string + instanceOCID string + } + type fields struct { + instanceSvcFn func(t *testing.T, args *args) cloud.OCIInstanceService + networkSvcFn func(t *testing.T, args *args) cloud.OCINetworkService + } + tests := []struct { + name string + fields fields + args args + want *core.Vnic + wantErr error + }{ + { + name: "get primary VNIC of instance", + fields: fields{ + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(&core.Vnic{ + Id: common.String("test-vnic-id"), + }, nil).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + want: &core.Vnic{ + Id: common.String("test-vnic-id"), + }, + }, + { + name: "failed to list VNIC attachments", + fields: fields{ + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return(nil, errors.New("error")).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + return cmocks.NewOCINetworkService(t) + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + wantErr: errors.New("failed to list VNIC attachments: error"), + }, + { + name: "failed to get primary VNIC of instance", + fields: fields{ + instanceSvcFn: func(t *testing.T, args *args) cloud.OCIInstanceService { + mockSvc := cmocks.NewOCIInstanceService(t) + mockSvc.EXPECT().ListVnicAttachments(mock.Anything, args.compartmentOCID, args.instanceOCID).Return([]core.VnicAttachment{ + {VnicId: common.String("test-vnic-id")}, + }, nil).Once() + return mockSvc + }, + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPrimaryVnic(mock.Anything, mock.Anything).Return(nil, errors.New("error")).Once() + return mockSvc + }, + }, + args: args{ + compartmentOCID: "test-compartment-id", + instanceOCID: "test-instance-id", + }, + wantErr: errors.New("failed to get primary VNIC: error"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &ociAssigner{ + instanceSvc: tt.fields.instanceSvcFn(t, &tt.args), + networkSvc: tt.fields.networkSvcFn(t, &tt.args), + compartmentOCID: tt.args.compartmentOCID, + } + got, err := a.getPrimaryVnicOfInstance(context.TODO(), tt.args.instanceOCID) + if !matchErr(err, tt.wantErr) { + t.Errorf("getPrimaryVnicOfInstance() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getPrimaryVnicOfInstance() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ociAssigner_handlePublicIPAlreadyAssignedCase(t *testing.T) { + type args struct { + vnic *core.Vnic + filter *types.OCIFilters + compartmentOCID string + } + type fields struct { + logger *logrus.Entry + networkSvcFn func(t *testing.T, args *args) cloud.OCINetworkService + } + tests := []struct { + name string + fields fields + args args + want bool + wantErr error + }{ + { + name: "no vnic", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + return cmocks.NewOCINetworkService(t) + }, + }, + want: false, + }, + { + name: "no public IP assigned", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + return cmocks.NewOCINetworkService(t) + }, + }, + args: args{ + vnic: &core.Vnic{}, + }, + want: false, + }, + { + name: "public IP already assigned from reserved list", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + Scope: core.ListPublicIpsScopeRegion, + CompartmentId: common.String(args.compartmentOCID), + }, args.filter).Return([]core.PublicIp{ + {IpAddress: common.String("1.2.3.4"), LifecycleState: core.PublicIpLifecycleStateAssigned}, + }, nil).Once() + return mockSvc + }, + }, + args: args{ + vnic: &core.Vnic{ + PublicIp: common.String("1.2.3.4"), + }, + compartmentOCID: "test-compartment-id", + }, + want: true, + }, + { + name: "failed to check public IP already assigned from reserved list", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + Scope: core.ListPublicIpsScopeRegion, + CompartmentId: common.String(args.compartmentOCID), + }, args.filter).Return([]core.PublicIp{}, errors.New("error")).Once() + return mockSvc + }, + }, + args: args{ + vnic: &core.Vnic{ + PublicIp: common.String("1.2.3.4"), + }, + compartmentOCID: "test-compartment-id", + }, + want: false, + wantErr: errors.New("failed to list reserved public IPs assigned to private IP: failed to list public IPs: error"), + }, + { + name: "public IP assigned but not from reserved list", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{}, nil).Once() + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + Scope: core.ListPublicIpsScopeRegion, + CompartmentId: common.String(args.compartmentOCID), + }, args.filter).Return([]core.PublicIp{ + {Id: common.String("test-public-ip"), IpAddress: common.String("1.2.3.4"), LifecycleState: core.PublicIpLifecycleStateAssigned}, + }, nil).Once() + mockSvc.EXPECT().UpdatePublicIP(mock.Anything, "test-public-ip", "").Return(nil).Once() + return mockSvc + }, + }, + args: args{ + vnic: &core.Vnic{ + PublicIp: common.String("1.2.3.4"), + }, + compartmentOCID: "test-compartment-id", + }, + want: false, + }, + { + name: "failed to list public IPs assigned to private IP", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{}, nil).Once() + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + Scope: core.ListPublicIpsScopeRegion, + CompartmentId: common.String(args.compartmentOCID), + }, args.filter).Return([]core.PublicIp{}, errors.New("error")).Once() + return mockSvc + }, + }, + args: args{ + vnic: &core.Vnic{ + PublicIp: common.String("1.2.3.4"), + }, + compartmentOCID: "test-compartment-id", + }, + want: false, + wantErr: errors.New("failed to list public IPs assigned to private IP: failed to list public IPs: error"), + }, + { + name: "failed to update public ip", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{}, nil).Once() + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + Scope: core.ListPublicIpsScopeRegion, + CompartmentId: common.String(args.compartmentOCID), + }, args.filter).Return([]core.PublicIp{ + {Id: common.String("test-public-ip"), IpAddress: common.String("1.2.3.4"), LifecycleState: core.PublicIpLifecycleStateAssigned}, + }, nil).Once() + mockSvc.EXPECT().UpdatePublicIP(mock.Anything, "test-public-ip", "").Return(errors.New("error")).Once() + return mockSvc + }, + }, + args: args{ + vnic: &core.Vnic{ + PublicIp: common.String("1.2.3.4"), + }, + compartmentOCID: "test-compartment-id", + }, + want: false, + wantErr: errors.New("failed to unassign public IP assigned to private IP: error"), + }, + { + name: "ephemeral public IP assigned", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{}, nil).Times(2) + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + Scope: core.ListPublicIpsScopeAvailabilityDomain, + CompartmentId: common.String(args.compartmentOCID), + AvailabilityDomain: common.String("test-availability-domain"), + }, args.filter).Return([]core.PublicIp{ + {Id: common.String("test-public-ip"), IpAddress: common.String("1.2.3.4"), LifecycleState: core.PublicIpLifecycleStateAssigned}, + }, nil).Once() + mockSvc.EXPECT().DeletePublicIP(mock.Anything, "test-public-ip").Return(nil).Once() + return mockSvc + }, + }, + args: args{ + vnic: &core.Vnic{ + PublicIp: common.String("1.2.3.4"), + AvailabilityDomain: common.String("test-availability-domain"), + }, + compartmentOCID: "test-compartment-id", + }, + want: false, + }, + { + name: "failed to list ephemeral public IPs", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{}, nil).Times(2) + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + Scope: core.ListPublicIpsScopeAvailabilityDomain, + CompartmentId: common.String(args.compartmentOCID), + AvailabilityDomain: common.String("test-availability-domain"), + }, args.filter).Return([]core.PublicIp{}, errors.New("error")).Once() + return mockSvc + }, + }, + args: args{ + vnic: &core.Vnic{ + PublicIp: common.String("1.2.3.4"), + AvailabilityDomain: common.String("test-availability-domain"), + }, + compartmentOCID: "test-compartment-id", + }, + want: false, + wantErr: errors.New("failed to list ephemeral public IPs assigned to private IP: failed to list ephemeral public IPs: error"), + }, + { + name: "failed to delete public IP", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{}, nil).Times(2) + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + Scope: core.ListPublicIpsScopeAvailabilityDomain, + CompartmentId: common.String(args.compartmentOCID), + AvailabilityDomain: common.String("test-availability-domain"), + }, args.filter).Return([]core.PublicIp{ + {Id: common.String("test-public-ip"), IpAddress: common.String("1.2.3.4"), LifecycleState: core.PublicIpLifecycleStateAssigned}, + }, nil).Once() + mockSvc.EXPECT().DeletePublicIP(mock.Anything, "test-public-ip").Return(errors.New("error")).Once() + return mockSvc + }, + }, + args: args{ + vnic: &core.Vnic{ + PublicIp: common.String("1.2.3.4"), + AvailabilityDomain: common.String("test-availability-domain"), + }, + compartmentOCID: "test-compartment-id", + }, + want: false, + wantErr: errors.New("failed to delete ephemeral public IP assigned to private IP: error"), + }, + { + name: "unhandled case", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{}, nil).Times(2) + return mockSvc + }, + }, + args: args{ + vnic: &core.Vnic{ + PublicIp: common.String("1.2.3.4"), + }, + compartmentOCID: "test-compartment-id", + filter: &types.OCIFilters{}, + }, + want: false, + wantErr: errors.New("availability domain not found"), + }, + { + name: "unhandled case", + fields: fields{ + logger: logrus.NewEntry(logrus.New()), + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, mock.Anything, mock.Anything).Return([]core.PublicIp{}, nil).Times(3) + return mockSvc + }, + }, + args: args{ + vnic: &core.Vnic{ + PublicIp: common.String("1.2.3.4"), + AvailabilityDomain: common.String("test-availability-domain"), + }, + compartmentOCID: "test-compartment-id", + }, + want: false, + wantErr: errors.New("unhandled case: public IP is assigned to the instance but not from the reserved IP list"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &ociAssigner{ + logger: tt.fields.logger, + networkSvc: tt.fields.networkSvcFn(t, &tt.args), + compartmentOCID: tt.args.compartmentOCID, + filters: tt.args.filter, + } + got, err := a.handlePublicIPAlreadyAssignedCase(context.TODO(), tt.args.vnic) + if !matchErr(err, tt.wantErr) { + t.Errorf("handlePublicIPAlreadyAssignedCase() error = %v, wantErr %v", err, tt.wantErr) + } + if got != tt.want { + t.Errorf("handlePublicIPAlreadyAssignedCase() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ociAssigner_fetchPublicIps(t *testing.T) { + type args struct { + useFilter bool + inUSe bool + compartmentOCID string + filters *types.OCIFilters + } + type fields struct { + networkSvcFn func(t *testing.T, args *args) cloud.OCINetworkService + } + tests := []struct { + name string + fields fields + args args + want []core.PublicIp + wantErr error + }{ + { + name: "fetch public IPs", + fields: fields{ + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + CompartmentId: common.String(args.compartmentOCID), + Scope: core.ListPublicIpsScopeRegion, + }, args.filters).Return([]core.PublicIp{ + {Id: common.String("test-public-ip-id"), IpAddress: common.String("test-ip-address"), LifecycleState: core.PublicIpLifecycleStateAvailable}, + }, nil).Once() + return mockSvc + }, + }, + args: args{ + useFilter: false, + inUSe: false, + compartmentOCID: "test-compartment-id", + }, + want: []core.PublicIp{ + {Id: common.String("test-public-ip-id"), IpAddress: common.String("test-ip-address"), LifecycleState: core.PublicIpLifecycleStateAvailable}, + }, + }, + { + name: "fetch public IPs with filter", + fields: fields{ + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + CompartmentId: common.String(args.compartmentOCID), + Scope: core.ListPublicIpsScopeRegion, + }, args.filters).Return([]core.PublicIp{ + {Id: common.String("test-public-ip-id"), IpAddress: common.String("test-ip-address"), LifecycleState: core.PublicIpLifecycleStateAvailable}, + }, nil).Once() + return mockSvc + }, + }, + args: args{ + useFilter: true, + inUSe: false, + compartmentOCID: "test-compartment-id", + filters: &types.OCIFilters{ + FreeformTags: map[string]string{"kubeip": "reserved"}, + }, + }, + want: []core.PublicIp{ + {Id: common.String("test-public-ip-id"), IpAddress: common.String("test-ip-address"), LifecycleState: core.PublicIpLifecycleStateAvailable}, + }, + }, + { + name: "fetch public IPs in use", + fields: fields{ + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + CompartmentId: common.String(args.compartmentOCID), + Scope: core.ListPublicIpsScopeRegion, + }, args.filters).Return([]core.PublicIp{ + {Id: common.String("test-public-ip-id"), IpAddress: common.String("test-ip-address"), LifecycleState: core.PublicIpLifecycleStateAssigned}, + }, nil).Once() + return mockSvc + }, + }, + args: args{ + useFilter: false, + inUSe: true, + compartmentOCID: "test-compartment-id", + }, + want: []core.PublicIp{ + {Id: common.String("test-public-ip-id"), IpAddress: common.String("test-ip-address"), LifecycleState: core.PublicIpLifecycleStateAssigned}, + }, + }, + { + name: "failed to fetch public IPs", + fields: fields{ + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + CompartmentId: common.String(args.compartmentOCID), + Scope: core.ListPublicIpsScopeRegion, + }, args.filters).Return([]core.PublicIp{}, errors.New("failed to list public IPs")).Once() + return mockSvc + }, + }, + args: args{ + useFilter: false, + inUSe: false, + compartmentOCID: "test-compartment-id", + }, + wantErr: errors.New("failed to list public IPs: failed to list public IPs"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &ociAssigner{ + networkSvc: tt.fields.networkSvcFn(t, &tt.args), + compartmentOCID: tt.args.compartmentOCID, + filters: tt.args.filters, + } + got, err := a.fetchPublicIps(context.TODO(), tt.args.useFilter, tt.args.inUSe) + if !matchErr(err, tt.wantErr) { + t.Errorf("OCI fetchPublicIps() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("OCI fetchPublicIps() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ociAssigner_fetchEphemeralPublicIps(t *testing.T) { + type args struct { + availabilityDomain string + compartmentOCID string + filters *types.OCIFilters + } + type fields struct { + networkSvcFn func(t *testing.T, args *args) cloud.OCINetworkService + } + tests := []struct { + name string + fields fields + args args + want []core.PublicIp + wantErr error + }{ + { + name: "fetch ephemeral public IPs", + fields: fields{ + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + CompartmentId: common.String(args.compartmentOCID), + Scope: core.ListPublicIpsScopeAvailabilityDomain, + AvailabilityDomain: common.String(args.availabilityDomain), + }, args.filters).Return([]core.PublicIp{ + {Id: common.String("test-public-ip-id"), IpAddress: common.String("test-ip-address")}, + }, nil).Once() + return mockSvc + }, + }, + args: args{ + availabilityDomain: "test-availability-domain", + compartmentOCID: "test-compartment-id", + }, + want: []core.PublicIp{ + {Id: common.String("test-public-ip-id"), IpAddress: common.String("test-ip-address")}, + }, + }, + { + name: "failed to fetch ephemeral public IPs", + fields: fields{ + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().ListPublicIps(mock.Anything, &core.ListPublicIpsRequest{ + CompartmentId: common.String(args.compartmentOCID), + Scope: core.ListPublicIpsScopeAvailabilityDomain, + AvailabilityDomain: common.String(args.availabilityDomain), + }, args.filters).Return([]core.PublicIp{}, errors.New("failed to list public IPs")).Once() + return mockSvc + }, + }, + args: args{ + availabilityDomain: "test-availability-domain", + compartmentOCID: "test-compartment-id", + }, + wantErr: errors.New("failed to list ephemeral public IPs: failed to list public IPs"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &ociAssigner{ + networkSvc: tt.fields.networkSvcFn(t, &tt.args), + compartmentOCID: tt.args.compartmentOCID, + filters: tt.args.filters, + } + got, err := a.fetchEphemeralPublicIPs(context.TODO(), tt.args.availabilityDomain) + if !matchErr(err, tt.wantErr) { + t.Errorf("OCI fetchEphemeralPublicIPs() error = %v, wantErr %v", err, tt.wantErr) + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("OCI fetchEphemeralPublicIPs() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_ociAssigner_tryAssignAddress(t *testing.T) { + type args struct { + privateIPOCID string + publicIP string + } + type fields struct { + networkSvcFn func(t *testing.T, args *args) cloud.OCINetworkService + } + tests := []struct { + name string + fields fields + args args + wantErr error + }{ + { + name: "assign public IP", + fields: fields{ + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPublicIP(mock.Anything, "test-public-ip-id").Return(&core.PublicIp{ + Id: common.String("test-public-ip-id"), + LifecycleState: core.PublicIpLifecycleStateAvailable, + }, nil).Once() + mockSvc.EXPECT().UpdatePublicIP(mock.Anything, "test-public-ip-id", "test-private-ip-id").Return(nil).Once() + return mockSvc + }, + }, + args: args{ + privateIPOCID: "test-private-ip-id", + publicIP: "test-public-ip-id", + }, + }, + { + name: "failed to get public ip details", + fields: fields{ + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPublicIP(mock.Anything, "test-public-ip-id").Return(nil, errors.New("invalid ip id")).Once() + return mockSvc + }, + }, + args: args{ + privateIPOCID: "test-private-ip-id", + publicIP: "test-public-ip-id", + }, + wantErr: errors.New("failed to get public IP details: invalid ip id"), + }, + { + name: "public IP detail is nil", + fields: fields{ + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPublicIP(mock.Anything, "test-public-ip-id").Return(nil, nil).Once() + return mockSvc + }, + }, + args: args{ + privateIPOCID: "test-private-ip-id", + publicIP: "test-public-ip-id", + }, + wantErr: errors.New("public IP not found"), + }, + { + name: "public IP is not available", + fields: fields{ + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPublicIP(mock.Anything, "test-public-ip-id").Return(&core.PublicIp{ + Id: common.String("test-public-ip-id"), + LifecycleState: core.PublicIpLifecycleStateAssigned, + }, nil).Once() + return mockSvc + }, + }, + args: args{ + privateIPOCID: "test-private-ip-id", + publicIP: "test-public-ip-id", + }, + wantErr: errors.New("public IP is not available"), + }, + { + name: "failed to update public IP", + fields: fields{ + networkSvcFn: func(t *testing.T, args *args) cloud.OCINetworkService { + mockSvc := cmocks.NewOCINetworkService(t) + mockSvc.EXPECT().GetPublicIP(mock.Anything, "test-public-ip-id").Return(&core.PublicIp{ + Id: common.String("test-public-ip-id"), + LifecycleState: core.PublicIpLifecycleStateAvailable, + }, nil).Once() + mockSvc.EXPECT().UpdatePublicIP(mock.Anything, "test-public-ip-id", "test-private-ip-id").Return(errors.New("error while update")).Once() + return mockSvc + }, + }, + args: args{ + privateIPOCID: "test-private-ip-id", + publicIP: "test-public-ip-id", + }, + wantErr: errors.New("failed to assign public IP: error while update"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := &ociAssigner{ + networkSvc: tt.fields.networkSvcFn(t, &tt.args), + } + if err := a.tryAssignAddress(context.TODO(), tt.args.privateIPOCID, tt.args.publicIP); !matchErr(err, tt.wantErr) { + t.Errorf("OCI tryAssignAddress() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func Test_parseOCIFilters(t *testing.T) { + tests := []struct { + name string + cfg *config.Config + want *types.OCIFilters + wantErr error + }{ + { + name: "no config", + wantErr: errors.New("config is nil"), + }, + { + name: "valid freeformTags filter", + cfg: &config.Config{ + Filter: []string{"freeformTags.key1=value1"}, + }, + want: &types.OCIFilters{ + FreeformTags: map[string]string{"key1": "value1"}, + }, + }, + { + name: "invalid freeformTags filter", + cfg: &config.Config{ + Filter: []string{"freeformTags.key1value1"}, + }, + wantErr: errors.New("failed to parse freeform tag filter: invalid filter format for freeform tags, should be in format freeformTags.key=value, found: freeformTags.key1value1"), + }, + { + name: "invalid filter format", + cfg: &config.Config{ + Filter: []string{"invalidFilter"}, + }, + wantErr: errors.New("invalid filter format for OCI, should be in format freeformTags.key=value or definedTags.Namespace.key=value, found: invalidFilter"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseOCIFilters(tt.cfg) + if !matchErr(err, tt.wantErr) { + t.Errorf("parseOCIFilters() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseOCIFilters() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/cloud/oci_instance_service.go b/internal/cloud/oci_instance_service.go new file mode 100644 index 0000000..f9a1362 --- /dev/null +++ b/internal/cloud/oci_instance_service.go @@ -0,0 +1,47 @@ +package cloud + +import ( + "context" + + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/core" + "github.com/pkg/errors" +) + +// OCIInstanceService is the interface for all instance related operations in OCI. +type OCIInstanceService interface { + ListVnicAttachments(ctx context.Context, compartmentOCID, instanceOCID string) ([]core.VnicAttachment, error) +} + +// ociInstanceService is the implementation of OCIInstanceService. +type ociInstanceService struct { + client core.ComputeClient +} + +// NewOCIInstanceService creates a new instance of OCIInstanceService. +func NewOCIInstanceService() (OCIInstanceService, error) { + client, err := core.NewComputeClientWithConfigurationProvider(common.DefaultConfigProvider()) + if err != nil { + return nil, errors.Wrap(err, "failed to create OCI Compute client") + } + + return &ociInstanceService{client: client}, nil +} + +// ListVnicAttachments lists all VNIC attachments for the given compartment and instance OCID. +func (svc *ociInstanceService) ListVnicAttachments(ctx context.Context, compartmentOCID, instanceOCID string) ([]core.VnicAttachment, error) { + request := core.ListVnicAttachmentsRequest{ + CompartmentId: common.String(compartmentOCID), + InstanceId: common.String(instanceOCID), + } + response, err := svc.client.ListVnicAttachments(ctx, request) + if err != nil { + return nil, errors.Wrap(err, "error while listing VNIC attachments") + } + + if response.Items == nil { + return nil, errors.Errorf("no VNIC attachments found, compartmentOCID: %s, instanceOCID: %s", compartmentOCID, instanceOCID) + } + + return response.Items, nil +} diff --git a/internal/cloud/oci_network_service.go b/internal/cloud/oci_network_service.go new file mode 100644 index 0000000..40a0173 --- /dev/null +++ b/internal/cloud/oci_network_service.go @@ -0,0 +1,144 @@ +package cloud + +import ( + "context" + + "github.com/doitintl/kubeip/internal/types" + "github.com/oracle/oci-go-sdk/v65/common" + "github.com/oracle/oci-go-sdk/v65/core" + "github.com/pkg/errors" +) + +// OCINetworkService is the interface for all network related operations in OCI (Virtual Network). +type OCINetworkService interface { + ListPublicIps(ctx context.Context, request *core.ListPublicIpsRequest, filters *types.OCIFilters) ([]core.PublicIp, error) + GetPublicIP(ctx context.Context, publicIPOCID string) (*core.PublicIp, error) + UpdatePublicIP(ctx context.Context, publicIPOCID, privateIPOCID string) error + DeletePublicIP(ctx context.Context, publicIPOCID string) error + GetPrimaryPrivateIPOfVnic(ctx context.Context, vnicOCID string) (*core.PrivateIp, error) + GetPrimaryVnic(ctx context.Context, vnicAttachments []core.VnicAttachment) (*core.Vnic, error) +} + +// ociNetworkService is the implementation of OCINetworkService. +type ociNetworkService struct { + client core.VirtualNetworkClient +} + +// NewOCINetworkService creates a new instance of OCINetworkService. +func NewOCINetworkService() (OCINetworkService, error) { + client, err := core.NewVirtualNetworkClientWithConfigurationProvider(common.DefaultConfigProvider()) + if err != nil { + return nil, errors.Wrap(err, "failed to create OCI Virtual Network client") + } + + return &ociNetworkService{client: client}, nil +} + +// ListPublicIps lists all public IPs for the given request and applies the given filters. +func (svc *ociNetworkService) ListPublicIps(ctx context.Context, request *core.ListPublicIpsRequest, filters *types.OCIFilters) ([]core.PublicIp, error) { + response, err := svc.client.ListPublicIps(ctx, *request) + if err != nil { + return nil, errors.Wrap(err, "failed to list public IPs") + } + + if response.Items == nil { + return nil, errors.New("no public IPs found") + } + + // Apply filters + if filters != nil { + list := []core.PublicIp{} + for _, ip := range response.Items { + if filters.CheckFreeformTagFilter(ip.FreeformTags) && filters.CheckDefinedTagFilter(ip.DefinedTags) { + list = append(list, ip) + } + } + return list, nil + } + + return response.Items, nil +} + +// GetPublicIP returns the public IP with the given OCID. +func (svc *ociNetworkService) GetPublicIP(ctx context.Context, publicIPOCID string) (*core.PublicIp, error) { + request := core.GetPublicIpRequest{ + PublicIpId: common.String(publicIPOCID), + } + response, err := svc.client.GetPublicIp(ctx, request) + if err != nil { + return nil, errors.Wrap(err, "failed to get details of public IP OCID: %s"+publicIPOCID) + } + + if response.PublicIp.Id == nil { + return nil, errors.Errorf("no public IP found with OCID %s", publicIPOCID) + } + + return &response.PublicIp, nil +} + +// UpdatePublicIP updates the public IP with the given OCID. +func (svc *ociNetworkService) UpdatePublicIP(ctx context.Context, publicIPOCID, privateIPOCID string) error { + request := core.UpdatePublicIpRequest{ + PublicIpId: common.String(publicIPOCID), + UpdatePublicIpDetails: core.UpdatePublicIpDetails{ + PrivateIpId: common.String(privateIPOCID), + }, + } + if _, err := svc.client.UpdatePublicIp(ctx, request); err != nil { + return errors.Wrap(err, "failed to update public IP") + } + + return nil +} + +// DeletePublicIP deletes the public IP with the given OCID. +func (svc *ociNetworkService) DeletePublicIP(ctx context.Context, publicIPOCID string) error { + request := core.DeletePublicIpRequest{ + PublicIpId: common.String(publicIPOCID), + } + if _, err := svc.client.DeletePublicIp(ctx, request); err != nil { + return errors.Wrap(err, "failed to delete public IP") + } + + return nil +} + +// GetPrimaryPrivateIPOfVnic returns the primary private IP of the given VNIC. +func (svc *ociNetworkService) GetPrimaryPrivateIPOfVnic(ctx context.Context, vnicOCID string) (*core.PrivateIp, error) { + request := core.ListPrivateIpsRequest{ + VnicId: common.String(vnicOCID), + } + response, err := svc.client.ListPrivateIps(ctx, request) + if err != nil { + return nil, errors.Wrap(err, "failed to list private IPs for the VNIC %s"+vnicOCID) + } + + if response.Items == nil { + return nil, errors.New("no private IPs found for the VNIC %s" + vnicOCID) + } + + // Loop through the private IPs and return the primary one + for _, privateIP := range response.Items { + if *privateIP.IsPrimary { + return &privateIP, nil + } + } + + return nil, errors.New("no primary private IP found for the VNIC %s" + vnicOCID) +} + +// GetPrimaryVnic returns the primary VNIC from the given VNIC attachments. +func (svc *ociNetworkService) GetPrimaryVnic(ctx context.Context, vnicAttachments []core.VnicAttachment) (*core.Vnic, error) { + for _, vnicAttachment := range vnicAttachments { + vnic, err := svc.client.GetVnic(ctx, core.GetVnicRequest{VnicId: vnicAttachment.VnicId}) + if err != nil { + return nil, errors.Wrap(err, "failed to get VNIC details with OCID %s"+*vnicAttachment.VnicId) + } + + if vnic.IsPrimary != nil && *vnic.IsPrimary { + return &vnic.Vnic, nil + } + } + + return nil, errors.New("no primary VNIC found from the given VNIC attachments") +} diff --git a/internal/config/config.go b/internal/config/config.go index c4fb238..e337af6 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,9 +11,9 @@ type Config struct { KubeConfigPath string `json:"kubeconfig"` // NodeName is the name of the Kubernetes node NodeName string `json:"node-name"` - // Project is the name of the GCP project or the AWS account ID + // Project is the name of the GCP project or the AWS account ID or the OCI compartment OCID Project string `json:"project"` - // Region is the name of the GCP region or the AWS region + // Region is the name of the GCP region or the AWS region or the OCI region Region string `json:"region"` // IPv6 support IPv6 bool `json:"ipv6"` diff --git a/internal/node/explorer.go b/internal/node/explorer.go index 8b0f0aa..b5ead09 100644 --- a/internal/node/explorer.go +++ b/internal/node/explorer.go @@ -19,6 +19,7 @@ const ( awsPoolLabel = "eks.amazonaws.com/nodegroup" azurePoolLabel = "node.kubernetes.io/instancegroup" gcpPoolLabel = "cloud.google.com/gke-nodepool" + ociPoolAnnotation = "oci.oraclecloud.com/node-pool-id" regionLabel = "topology.kubernetes.io/region" zoneLabel = "topology.kubernetes.io/zone" ) @@ -56,10 +57,22 @@ func getCloudProvider(providerID string) (types.CloudProvider, error) { if strings.HasPrefix(providerID, "gce://") { return types.CloudProviderGCP, nil } - return "", errors.Errorf("unsupported cloud provider: %s", providerID) + if strings.HasPrefix(providerID, "oci") { + return types.CloudProviderOCI, nil + } + return "", errors.Errorf("unsupported provider ID: %s", providerID) } func getInstance(providerID string) (string, error) { + if providerID == "" { + return "", errors.Errorf("failed to get instance ID, provider ID is empty") + } + + // In case of OCI, the provider ID is the instance ID + if strings.HasPrefix(providerID, "oci") { + return providerID, nil + } + s := strings.Split(providerID, "/") if len(s) < minProviderIDTokens { return "", errors.Errorf("failed to get instance ID") @@ -67,7 +80,12 @@ func getInstance(providerID string) (string, error) { return s[len(s)-1], nil } -func getNodePool(providerID types.CloudProvider, labels map[string]string) (string, error) { +func getNodePool(providerID types.CloudProvider, node *v1.Node) (string, error) { + if node == nil { + return "", errors.Errorf("node info is nil") + } + labels := node.Labels + annotations := node.Annotations var ok bool var pool string if providerID == types.CloudProviderAWS { @@ -76,6 +94,8 @@ func getNodePool(providerID types.CloudProvider, labels map[string]string) (stri pool, ok = labels[azurePoolLabel] } else if providerID == types.CloudProviderGCP { pool, ok = labels[gcpPoolLabel] + } else if providerID == types.CloudProviderOCI { + pool, ok = annotations[ociPoolAnnotation] } else { return "", errors.Errorf("unsupported cloud provider: %s", providerID) } @@ -107,6 +127,10 @@ func getAddresses(addresses []v1.NodeAddress) ([]net.IP, []net.IP, error) { // GetNode returns the node object func (d *explorer) GetNode(ctx context.Context, nodeName string) (*types.Node, error) { + if d.client == nil { + return nil, errors.Errorf("kubernetes client is nil") + } + // get node name from downward API if nodeName is empty if nodeName == "" { var err error @@ -146,8 +170,8 @@ func (d *explorer) GetNode(ctx context.Context, nodeName string) (*types.Node, e return nil, errors.Errorf("failed to get node zone") } - // get node pool from node labels - pool, err := getNodePool(cloudProvider, n.Labels) + // get node pool from node + pool, err := getNodePool(cloudProvider, n) if err != nil { return nil, errors.Wrap(err, "failed to get node pool") } diff --git a/internal/node/explorer_test.go b/internal/node/explorer_test.go index 89a1a9b..064d601 100644 --- a/internal/node/explorer_test.go +++ b/internal/node/explorer_test.go @@ -8,12 +8,25 @@ import ( "testing" "github.com/doitintl/kubeip/internal/types" + "github.com/pkg/errors" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" ) +func matchErr(err1 error, err2 error) bool { + errStr1 := "" + errStr2 := "" + if err1 != nil { + errStr1 = err1.Error() + } + if err2 != nil { + errStr2 = err2.Error() + } + return errStr1 == errStr2 +} + func Test_getNodeName(t *testing.T) { tests := []struct { name string @@ -88,7 +101,7 @@ func Test_getCloudProvider(t *testing.T) { name string args args want types.CloudProvider - wantErr bool + wantErr error }{ { name: "aws", @@ -111,18 +124,25 @@ func Test_getCloudProvider(t *testing.T) { }, want: types.CloudProviderGCP, }, + { + name: "oci", + args: args{ + providerID: "ocid1.instance.oc1.ap-mumbai-1.anrg6ljrdgsxvfacnncnwaxaasbdnjdgwuejhkbdfejkenoernoered", + }, + want: types.CloudProviderOCI, + }, { name: "unsupported", args: args{ providerID: "unsupported", }, - wantErr: true, + wantErr: errors.New("unsupported provider ID: unsupported"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := getCloudProvider(tt.args.providerID) - if (err != nil) != tt.wantErr { + if !matchErr(err, tt.wantErr) { t.Errorf("getCloudProvider() error = %v, wantErr %v", err, tt.wantErr) return } @@ -136,21 +156,43 @@ func Test_getCloudProvider(t *testing.T) { func Test_getNodePool(t *testing.T) { type args struct { providerID types.CloudProvider - labels map[string]string + node *v1.Node } tests := []struct { name string args args want string - wantErr bool + wantErr error }{ + { + name: "nil node", + wantErr: errors.New("node info is nil"), + }, + { + name: "oci", + args: args{ + providerID: types.CloudProviderOCI, + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "oci.oraclecloud.com/node-pool-id": "ocid1.nodepool.oc1.ap-mumbai-1.aaaaaaaa7yv75wqblfix5rxnylajo35y3wabren", + }, + }, + }, + }, + want: "ocid1.nodepool.oc1.ap-mumbai-1.aaaaaaaa7yv75wqblfix5rxnylajo35y3wabren", + }, { name: "aws", args: args{ providerID: types.CloudProviderAWS, - labels: map[string]string{ - "eks.amazonaws.com/nodegroup": "test-node-pool", - "beta.kubernetes.io/os": "linux", + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "eks.amazonaws.com/nodegroup": "test-node-pool", + "beta.kubernetes.io/os": "linux", + }, + }, }, }, want: "test-node-pool", @@ -159,9 +201,13 @@ func Test_getNodePool(t *testing.T) { name: "azure", args: args{ providerID: types.CloudProviderAzure, - labels: map[string]string{ - "node.kubernetes.io/instancegroup": "test-node-pool", - "beta.kubernetes.io/os": "linux", + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "node.kubernetes.io/instancegroup": "test-node-pool", + "beta.kubernetes.io/os": "linux", + }, + }, }, }, want: "test-node-pool", @@ -170,9 +216,13 @@ func Test_getNodePool(t *testing.T) { name: "gcp", args: args{ providerID: types.CloudProviderGCP, - labels: map[string]string{ - "cloud.google.com/gke-nodepool": "test-node-pool", - "beta.kubernetes.io/os": "linux", + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "cloud.google.com/gke-nodepool": "test-node-pool", + "beta.kubernetes.io/os": "linux", + }, + }, }, }, want: "test-node-pool", @@ -181,27 +231,47 @@ func Test_getNodePool(t *testing.T) { name: "unsupported", args: args{ providerID: "unsupported", - labels: map[string]string{ - "beta.kubernetes.io/os": "linux", + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "beta.kubernetes.io/os": "linux", + }, + }, }, }, - wantErr: true, + wantErr: errors.New("unsupported cloud provider: unsupported"), }, { name: "no node pool", args: args{ providerID: types.CloudProviderAWS, - labels: map[string]string{ - "beta.kubernetes.io/os": "linux", + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "beta.kubernetes.io/os": "linux", + }, + }, }, }, - wantErr: true, + wantErr: errors.New("failed to get node pool"), + }, + { + name: "no node pool oci", + args: args{ + providerID: types.CloudProviderOCI, + node: &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, + }, + }, + wantErr: errors.New("failed to get node pool"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := getNodePool(tt.args.providerID, tt.args.labels) - if (err != nil) != tt.wantErr { + got, err := getNodePool(tt.args.providerID, tt.args.node) + if !matchErr(err, tt.wantErr) { t.Errorf("getNodePool() error = %v, wantErr %v", err, tt.wantErr) return } @@ -312,8 +382,27 @@ func Test_explorer_GetNode(t *testing.T) { fields fields args args want *types.Node - wantErr bool + wantErr error }{ + { + name: "nil client", + wantErr: errors.New("kubernetes client is nil"), + }, + { + name: "empty nodename", + fields: fields{client: fake.NewSimpleClientset()}, + wantErr: errors.New("failed to get node name from downward API: failed to read /etc/podinfo/nodeName: open /etc/podinfo/nodeName: no such file or directory"), + }, + { + name: "failed to get node", + fields: fields{ + client: fake.NewSimpleClientset(), + }, + args: args{ + nodeName: "test-node", + }, + wantErr: errors.New("failed to get kubernetes node: nodes \"test-node\" not found"), + }, { name: "get node", fields: fields{ @@ -356,6 +445,63 @@ func Test_explorer_GetNode(t *testing.T) { }, }, }, + { + name: "get node oci", + fields: fields{ + client: fake.NewSimpleClientset(&v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + Annotations: map[string]string{ + "oci.oraclecloud.com/node-pool-id": "ocid1.nodepool.oc1.ap-mumbai-1.test", + }, + Labels: map[string]string{ + "topology.kubernetes.io/region": "us-west-2", + "topology.kubernetes.io/zone": "us-west-2b", + }, + }, + Spec: v1.NodeSpec{ + ProviderID: "ocid1.instance.oc1.ap-mumbai-1.test", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeExternalIP, Address: "132.10.10.1"}, + {Type: v1.NodeInternalIP, Address: "10.10.0.1"}, + }, + }, + }), + }, + args: args{ + nodeName: "test-node", + }, + want: &types.Node{ + Name: "test-node", + Instance: "ocid1.instance.oc1.ap-mumbai-1.test", + Cloud: types.CloudProviderOCI, + Pool: "ocid1.nodepool.oc1.ap-mumbai-1.test", + Region: "us-west-2", + Zone: "us-west-2b", + ExternalIPs: []net.IP{ + net.ParseIP("132.10.10.1"), + }, + InternalIPs: []net.IP{ + net.ParseIP("10.10.0.1"), + }, + }, + }, + { + name: "failed to get cloud provider", + fields: fields{ + client: fake.NewSimpleClientset(&v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + }, + }), + }, + args: args{ + nodeName: "test-node", + }, + wantErr: errors.New("failed to get cloud provider: unsupported provider ID: "), + }, { name: "failed to get region", fields: fields{ @@ -375,7 +521,7 @@ func Test_explorer_GetNode(t *testing.T) { args: args{ nodeName: "test-node", }, - wantErr: true, + wantErr: errors.New("failed to get node region"), }, { name: "failed to get zone", @@ -384,8 +530,9 @@ func Test_explorer_GetNode(t *testing.T) { ObjectMeta: metav1.ObjectMeta{ Name: "test-node", Labels: map[string]string{ - "eks.amazonaws.com/nodegroup": "test-node-pool", - "beta.kubernetes.io/os": "linux", + "eks.amazonaws.com/nodegroup": "test-node-pool", + "beta.kubernetes.io/os": "linux", + "topology.kubernetes.io/region": "asia-south1", }, }, Spec: v1.NodeSpec{ @@ -396,7 +543,64 @@ func Test_explorer_GetNode(t *testing.T) { args: args{ nodeName: "test-node", }, - wantErr: true, + wantErr: errors.New("failed to get node zone"), + }, + { + name: "failed to get node pool", + fields: fields{ + client: fake.NewSimpleClientset(&v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + Labels: map[string]string{ + "beta.kubernetes.io/os": "linux", + "topology.kubernetes.io/region": "us-west-2", + "topology.kubernetes.io/zone": "us-west-2b", + }, + }, + Spec: v1.NodeSpec{ + ProviderID: "aws:///us-west-2b/i-06d71a5ffc05cc325", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeExternalIP, Address: "132.10.10.1"}, + {Type: v1.NodeInternalIP, Address: "10.10.0.1"}, + }, + }, + }), + }, + args: args{ + nodeName: "test-node", + }, + wantErr: errors.New("failed to get node pool: failed to get node pool"), + }, + { + name: "failed to get node addresses", + fields: fields{ + client: fake.NewSimpleClientset(&v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-node", + Labels: map[string]string{ + "eks.amazonaws.com/nodegroup": "test-node-pool", + "beta.kubernetes.io/os": "linux", + "topology.kubernetes.io/region": "us-west-2", + "topology.kubernetes.io/zone": "us-west-2b", + }, + }, + Spec: v1.NodeSpec{ + ProviderID: "aws:///us-west-2b/i-06d71a5ffc05cc325", + }, + Status: v1.NodeStatus{ + Addresses: []v1.NodeAddress{ + {Type: v1.NodeExternalIP, Address: "132.10.10.1"}, + {Type: v1.NodeInternalIP, Address: "address"}, + }, + }, + }), + }, + args: args{ + nodeName: "test-node", + }, + wantErr: errors.New("failed to get node addresses: failed to parse IP address: address"), }, } for _, tt := range tests { @@ -405,7 +609,7 @@ func Test_explorer_GetNode(t *testing.T) { client: tt.fields.client, } got, err := d.GetNode(context.Background(), tt.args.nodeName) - if (err != nil) != tt.wantErr { + if !matchErr(err, tt.wantErr) { t.Errorf("GetNode() error = %v, wantErr %v", err, tt.wantErr) return } @@ -424,8 +628,22 @@ func Test_getInstance(t *testing.T) { name string args args want string - wantErr bool + wantErr error }{ + { + name: "empty provider ID", + args: args{ + providerID: "", + }, + wantErr: errors.New("failed to get instance ID, provider ID is empty"), + }, + { + name: "oci", + args: args{ + providerID: "ocid1.instance.oc1.ap-mumbai-1.anrg6ljrdgsxvfacnncnwaxaasbdnjdgwuejhkbdfejkenoernoered", + }, + want: "ocid1.instance.oc1.ap-mumbai-1.anrg6ljrdgsxvfacnncnwaxaasbdnjdgwuejhkbdfejkenoernoered", + }, { name: "aws", args: args{ @@ -452,13 +670,13 @@ func Test_getInstance(t *testing.T) { args: args{ providerID: "unsupported", }, - wantErr: true, + wantErr: errors.New("failed to get instance ID"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := getInstance(tt.args.providerID) - if (err != nil) != tt.wantErr { + if !matchErr(err, tt.wantErr) { t.Errorf("getInstance() error = %v, wantErr %v", err, tt.wantErr) return } diff --git a/internal/types/filters.go b/internal/types/filters.go new file mode 100644 index 0000000..f7e3e76 --- /dev/null +++ b/internal/types/filters.go @@ -0,0 +1,64 @@ +package types + +import ( + "strings" + + "github.com/pkg/errors" +) + +// OCIFilters is a struct that holds the filters for the OCI. +type OCIFilters struct { + FreeformTags map[string]string + DefinedTags map[string]map[string]interface{} +} + +// CheckFreeformTagFilter checks if the target contains all the filter keys and values. +func (f *OCIFilters) CheckFreeformTagFilter(target map[string]string) bool { + // If the filter is nil, return true, since there is no filter to apply + if f.FreeformTags == nil { + return true + } + + // If the target is nil, return false, since filter cannot be applied + if target == nil { + return false + } + + // Loop through the filter map and check if the target map contains all the filter keys and values + for key, value := range f.FreeformTags { + if val, ok := target[key]; !ok || val != value { + return false + } + } + return true +} + +// ParseFreeformTagFilter parses the filter string for freeform tags. +// Filter should be in following format: +// - "freeformTags.key=value" +func ParseFreeformTagFilter(filter string) (string, string, error) { + f := filter + if strings.HasPrefix(f, "freeformTags.") { + f = strings.TrimPrefix(f, "freeformTags.") + if split := strings.Split(f, "="); len(split) == 2 { //nolint:gomnd + return split[0], split[1], nil + } + } + + return "", "", errors.New("invalid filter format for freeform tags, should be in format freeformTags.key=value, found: " + filter) +} + +// ParseDefinedTagFilter parses the filter string for defined tags. +// Filter should be in following format: +// - "definedTags.Namespace.key=value" +// +// TODO: Add filter support for DefinedTags +func ParseDefinedTagFilter(_ string) (string, string, string, error) { + return "", "", "", nil +} + +// CheckDefinedTagFilter checks if the target contains all the filter keys and values. +// TODO: Add filter support for DefinedTags +func (f *OCIFilters) CheckDefinedTagFilter(_ map[string]map[string]interface{}) bool { + return true +} diff --git a/internal/types/filters_test.go b/internal/types/filters_test.go new file mode 100644 index 0000000..7debbda --- /dev/null +++ b/internal/types/filters_test.go @@ -0,0 +1,96 @@ +package types + +import ( + "testing" + + "github.com/pkg/errors" +) + +func Test_types_CheckFreeformTagFilter(t *testing.T) { + tests := []struct { + name string + filter OCIFilters + target map[string]string + want bool + }{ + { + name: "nil filter", + filter: OCIFilters{FreeformTags: nil}, + target: map[string]string{"key1": "value1"}, + want: true, + }, + { + name: "nil target", + filter: OCIFilters{FreeformTags: map[string]string{"key1": "value1"}}, + target: nil, + want: false, + }, + { + name: "matching filter", + filter: OCIFilters{FreeformTags: map[string]string{"key1": "value1"}}, + target: map[string]string{"key1": "value1"}, + want: true, + }, + { + name: "non-matching filter", + filter: OCIFilters{FreeformTags: map[string]string{"key1": "value1"}}, + target: map[string]string{"key1": "value2"}, + want: false, + }, + { + name: "partial match", + filter: OCIFilters{FreeformTags: map[string]string{"key1": "value1", "key2": "value2"}}, + target: map[string]string{"key1": "value1"}, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.filter.CheckFreeformTagFilter(tt.target); got != tt.want { + t.Errorf("CheckFreeformTagFilter() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_types_ParseFreeformTagFilter(t *testing.T) { + tests := []struct { + name string + filter string + wantKey string + wantVal string + wantErr error + }{ + { + name: "valid filter", + filter: "freeformTags.key=value", + wantKey: "key", + wantVal: "value", + wantErr: nil, + }, + { + name: "invalid filter format", + filter: "freeformTags.keyvalue", + wantKey: "", + wantVal: "", + wantErr: errors.New("invalid filter format for freeform tags, should be in format freeformTags.key=value, found: freeformTags.keyvalue"), + }, + { + name: "missing prefix", + filter: "key=value", + wantKey: "", + wantVal: "", + wantErr: errors.New("invalid filter format for freeform tags, should be in format freeformTags.key=value, found: key=value"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotKey, gotVal, err := ParseFreeformTagFilter(tt.filter) + if gotKey != tt.wantKey || gotVal != tt.wantVal || (err != nil && err.Error() != tt.wantErr.Error()) { + t.Errorf("ParseFreeformTagFilter() = (%v, %v, %v), want (%v, %v, %v)", gotKey, gotVal, err, tt.wantKey, tt.wantVal, tt.wantErr) + } + }) + } +} diff --git a/internal/types/node.go b/internal/types/node.go index 286cf1f..4321190 100644 --- a/internal/types/node.go +++ b/internal/types/node.go @@ -10,6 +10,7 @@ type CloudProvider string const ( CloudProviderGCP CloudProvider = "gcp" CloudProviderAWS CloudProvider = "aws" + CloudProviderOCI CloudProvider = "oci" CloudProviderAzure CloudProvider = "azure" ) diff --git a/mocks/cloud/OCIInstanceService.go b/mocks/cloud/OCIInstanceService.go new file mode 100644 index 0000000..16596c6 --- /dev/null +++ b/mocks/cloud/OCIInstanceService.go @@ -0,0 +1,97 @@ +// Code generated by mockery v2.30.16. DO NOT EDIT. + +package mocks + +import ( + context "context" + + core "github.com/oracle/oci-go-sdk/v65/core" + mock "github.com/stretchr/testify/mock" +) + +// OCIInstanceService is an autogenerated mock type for the OCIInstanceService type +type OCIInstanceService struct { + mock.Mock +} + +type OCIInstanceService_Expecter struct { + mock *mock.Mock +} + +func (_m *OCIInstanceService) EXPECT() *OCIInstanceService_Expecter { + return &OCIInstanceService_Expecter{mock: &_m.Mock} +} + +// ListVnicAttachments provides a mock function with given fields: ctx, compartmentOCID, instanceOCID +func (_m *OCIInstanceService) ListVnicAttachments(ctx context.Context, compartmentOCID string, instanceOCID string) ([]core.VnicAttachment, error) { + ret := _m.Called(ctx, compartmentOCID, instanceOCID) + + if len(ret) == 0 { + panic("no return value specified for ListVnicAttachments") + } + + var r0 []core.VnicAttachment + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]core.VnicAttachment, error)); ok { + return rf(ctx, compartmentOCID, instanceOCID) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) []core.VnicAttachment); ok { + r0 = rf(ctx, compartmentOCID, instanceOCID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]core.VnicAttachment) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, compartmentOCID, instanceOCID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OCIInstanceService_ListVnicAttachments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListVnicAttachments' +type OCIInstanceService_ListVnicAttachments_Call struct { + *mock.Call +} + +// ListVnicAttachments is a helper method to define mock.On call +// - ctx context.Context +// - compartmentOCID string +// - instanceOCID string +func (_e *OCIInstanceService_Expecter) ListVnicAttachments(ctx interface{}, compartmentOCID interface{}, instanceOCID interface{}) *OCIInstanceService_ListVnicAttachments_Call { + return &OCIInstanceService_ListVnicAttachments_Call{Call: _e.mock.On("ListVnicAttachments", ctx, compartmentOCID, instanceOCID)} +} + +func (_c *OCIInstanceService_ListVnicAttachments_Call) Run(run func(ctx context.Context, compartmentOCID string, instanceOCID string)) *OCIInstanceService_ListVnicAttachments_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *OCIInstanceService_ListVnicAttachments_Call) Return(_a0 []core.VnicAttachment, _a1 error) *OCIInstanceService_ListVnicAttachments_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *OCIInstanceService_ListVnicAttachments_Call) RunAndReturn(run func(context.Context, string, string) ([]core.VnicAttachment, error)) *OCIInstanceService_ListVnicAttachments_Call { + _c.Call.Return(run) + return _c +} + +// NewOCIInstanceService creates a new instance of OCIInstanceService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewOCIInstanceService(t interface { + mock.TestingT + Cleanup(func()) +}) *OCIInstanceService { + mock := &OCIInstanceService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/cloud/OCINetworkService.go b/mocks/cloud/OCINetworkService.go new file mode 100644 index 0000000..55aaa7f --- /dev/null +++ b/mocks/cloud/OCINetworkService.go @@ -0,0 +1,371 @@ +// Code generated by mockery v2.30.16. DO NOT EDIT. + +package mocks + +import ( + context "context" + + core "github.com/oracle/oci-go-sdk/v65/core" + mock "github.com/stretchr/testify/mock" + + types "github.com/doitintl/kubeip/internal/types" +) + +// OCINetworkService is an autogenerated mock type for the OCINetworkService type +type OCINetworkService struct { + mock.Mock +} + +type OCINetworkService_Expecter struct { + mock *mock.Mock +} + +func (_m *OCINetworkService) EXPECT() *OCINetworkService_Expecter { + return &OCINetworkService_Expecter{mock: &_m.Mock} +} + +// DeletePublicIP provides a mock function with given fields: ctx, publicIPOCID +func (_m *OCINetworkService) DeletePublicIP(ctx context.Context, publicIPOCID string) error { + ret := _m.Called(ctx, publicIPOCID) + + if len(ret) == 0 { + panic("no return value specified for DeletePublicIP") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, publicIPOCID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// OCINetworkService_DeletePublicIP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeletePublicIP' +type OCINetworkService_DeletePublicIP_Call struct { + *mock.Call +} + +// DeletePublicIP is a helper method to define mock.On call +// - ctx context.Context +// - publicIPOCID string +func (_e *OCINetworkService_Expecter) DeletePublicIP(ctx interface{}, publicIPOCID interface{}) *OCINetworkService_DeletePublicIP_Call { + return &OCINetworkService_DeletePublicIP_Call{Call: _e.mock.On("DeletePublicIP", ctx, publicIPOCID)} +} + +func (_c *OCINetworkService_DeletePublicIP_Call) Run(run func(ctx context.Context, publicIPOCID string)) *OCINetworkService_DeletePublicIP_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *OCINetworkService_DeletePublicIP_Call) Return(_a0 error) *OCINetworkService_DeletePublicIP_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *OCINetworkService_DeletePublicIP_Call) RunAndReturn(run func(context.Context, string) error) *OCINetworkService_DeletePublicIP_Call { + _c.Call.Return(run) + return _c +} + +// GetPrimaryPrivateIPOfVnic provides a mock function with given fields: ctx, vnicOCID +func (_m *OCINetworkService) GetPrimaryPrivateIPOfVnic(ctx context.Context, vnicOCID string) (*core.PrivateIp, error) { + ret := _m.Called(ctx, vnicOCID) + + if len(ret) == 0 { + panic("no return value specified for GetPrimaryPrivateIPOfVnic") + } + + var r0 *core.PrivateIp + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*core.PrivateIp, error)); ok { + return rf(ctx, vnicOCID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *core.PrivateIp); ok { + r0 = rf(ctx, vnicOCID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*core.PrivateIp) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, vnicOCID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OCINetworkService_GetPrimaryPrivateIPOfVnic_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPrimaryPrivateIPOfVnic' +type OCINetworkService_GetPrimaryPrivateIPOfVnic_Call struct { + *mock.Call +} + +// GetPrimaryPrivateIPOfVnic is a helper method to define mock.On call +// - ctx context.Context +// - vnicOCID string +func (_e *OCINetworkService_Expecter) GetPrimaryPrivateIPOfVnic(ctx interface{}, vnicOCID interface{}) *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call { + return &OCINetworkService_GetPrimaryPrivateIPOfVnic_Call{Call: _e.mock.On("GetPrimaryPrivateIPOfVnic", ctx, vnicOCID)} +} + +func (_c *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call) Run(run func(ctx context.Context, vnicOCID string)) *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call) Return(_a0 *core.PrivateIp, _a1 error) *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call) RunAndReturn(run func(context.Context, string) (*core.PrivateIp, error)) *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call { + _c.Call.Return(run) + return _c +} + +// GetPrimaryVnic provides a mock function with given fields: ctx, vnicAttachments +func (_m *OCINetworkService) GetPrimaryVnic(ctx context.Context, vnicAttachments []core.VnicAttachment) (*core.Vnic, error) { + ret := _m.Called(ctx, vnicAttachments) + + if len(ret) == 0 { + panic("no return value specified for GetPrimaryVnic") + } + + var r0 *core.Vnic + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []core.VnicAttachment) (*core.Vnic, error)); ok { + return rf(ctx, vnicAttachments) + } + if rf, ok := ret.Get(0).(func(context.Context, []core.VnicAttachment) *core.Vnic); ok { + r0 = rf(ctx, vnicAttachments) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*core.Vnic) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []core.VnicAttachment) error); ok { + r1 = rf(ctx, vnicAttachments) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OCINetworkService_GetPrimaryVnic_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPrimaryVnic' +type OCINetworkService_GetPrimaryVnic_Call struct { + *mock.Call +} + +// GetPrimaryVnic is a helper method to define mock.On call +// - ctx context.Context +// - vnicAttachments []core.VnicAttachment +func (_e *OCINetworkService_Expecter) GetPrimaryVnic(ctx interface{}, vnicAttachments interface{}) *OCINetworkService_GetPrimaryVnic_Call { + return &OCINetworkService_GetPrimaryVnic_Call{Call: _e.mock.On("GetPrimaryVnic", ctx, vnicAttachments)} +} + +func (_c *OCINetworkService_GetPrimaryVnic_Call) Run(run func(ctx context.Context, vnicAttachments []core.VnicAttachment)) *OCINetworkService_GetPrimaryVnic_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]core.VnicAttachment)) + }) + return _c +} + +func (_c *OCINetworkService_GetPrimaryVnic_Call) Return(_a0 *core.Vnic, _a1 error) *OCINetworkService_GetPrimaryVnic_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *OCINetworkService_GetPrimaryVnic_Call) RunAndReturn(run func(context.Context, []core.VnicAttachment) (*core.Vnic, error)) *OCINetworkService_GetPrimaryVnic_Call { + _c.Call.Return(run) + return _c +} + +// GetPublicIP provides a mock function with given fields: ctx, publicIPOCID +func (_m *OCINetworkService) GetPublicIP(ctx context.Context, publicIPOCID string) (*core.PublicIp, error) { + ret := _m.Called(ctx, publicIPOCID) + + if len(ret) == 0 { + panic("no return value specified for GetPublicIP") + } + + var r0 *core.PublicIp + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (*core.PublicIp, error)); ok { + return rf(ctx, publicIPOCID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *core.PublicIp); ok { + r0 = rf(ctx, publicIPOCID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*core.PublicIp) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, publicIPOCID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OCINetworkService_GetPublicIP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPublicIP' +type OCINetworkService_GetPublicIP_Call struct { + *mock.Call +} + +// GetPublicIP is a helper method to define mock.On call +// - ctx context.Context +// - publicIPOCID string +func (_e *OCINetworkService_Expecter) GetPublicIP(ctx interface{}, publicIPOCID interface{}) *OCINetworkService_GetPublicIP_Call { + return &OCINetworkService_GetPublicIP_Call{Call: _e.mock.On("GetPublicIP", ctx, publicIPOCID)} +} + +func (_c *OCINetworkService_GetPublicIP_Call) Run(run func(ctx context.Context, publicIPOCID string)) *OCINetworkService_GetPublicIP_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *OCINetworkService_GetPublicIP_Call) Return(_a0 *core.PublicIp, _a1 error) *OCINetworkService_GetPublicIP_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *OCINetworkService_GetPublicIP_Call) RunAndReturn(run func(context.Context, string) (*core.PublicIp, error)) *OCINetworkService_GetPublicIP_Call { + _c.Call.Return(run) + return _c +} + +// ListPublicIps provides a mock function with given fields: ctx, request, filters +func (_m *OCINetworkService) ListPublicIps(ctx context.Context, request *core.ListPublicIpsRequest, filters *types.OCIFilters) ([]core.PublicIp, error) { + ret := _m.Called(ctx, request, filters) + + if len(ret) == 0 { + panic("no return value specified for ListPublicIps") + } + + var r0 []core.PublicIp + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *core.ListPublicIpsRequest, *types.OCIFilters) ([]core.PublicIp, error)); ok { + return rf(ctx, request, filters) + } + if rf, ok := ret.Get(0).(func(context.Context, *core.ListPublicIpsRequest, *types.OCIFilters) []core.PublicIp); ok { + r0 = rf(ctx, request, filters) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]core.PublicIp) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *core.ListPublicIpsRequest, *types.OCIFilters) error); ok { + r1 = rf(ctx, request, filters) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// OCINetworkService_ListPublicIps_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPublicIps' +type OCINetworkService_ListPublicIps_Call struct { + *mock.Call +} + +// ListPublicIps is a helper method to define mock.On call +// - ctx context.Context +// - request *core.ListPublicIpsRequest +// - filters *types.OCIFilters +func (_e *OCINetworkService_Expecter) ListPublicIps(ctx interface{}, request interface{}, filters interface{}) *OCINetworkService_ListPublicIps_Call { + return &OCINetworkService_ListPublicIps_Call{Call: _e.mock.On("ListPublicIps", ctx, request, filters)} +} + +func (_c *OCINetworkService_ListPublicIps_Call) Run(run func(ctx context.Context, request *core.ListPublicIpsRequest, filters *types.OCIFilters)) *OCINetworkService_ListPublicIps_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*core.ListPublicIpsRequest), args[2].(*types.OCIFilters)) + }) + return _c +} + +func (_c *OCINetworkService_ListPublicIps_Call) Return(_a0 []core.PublicIp, _a1 error) *OCINetworkService_ListPublicIps_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *OCINetworkService_ListPublicIps_Call) RunAndReturn(run func(context.Context, *core.ListPublicIpsRequest, *types.OCIFilters) ([]core.PublicIp, error)) *OCINetworkService_ListPublicIps_Call { + _c.Call.Return(run) + return _c +} + +// UpdatePublicIP provides a mock function with given fields: ctx, publicIPOCID, privateIPOCID +func (_m *OCINetworkService) UpdatePublicIP(ctx context.Context, publicIPOCID string, privateIPOCID string) error { + ret := _m.Called(ctx, publicIPOCID, privateIPOCID) + + if len(ret) == 0 { + panic("no return value specified for UpdatePublicIP") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, publicIPOCID, privateIPOCID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// OCINetworkService_UpdatePublicIP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdatePublicIP' +type OCINetworkService_UpdatePublicIP_Call struct { + *mock.Call +} + +// UpdatePublicIP is a helper method to define mock.On call +// - ctx context.Context +// - publicIPOCID string +// - privateIPOCID string +func (_e *OCINetworkService_Expecter) UpdatePublicIP(ctx interface{}, publicIPOCID interface{}, privateIPOCID interface{}) *OCINetworkService_UpdatePublicIP_Call { + return &OCINetworkService_UpdatePublicIP_Call{Call: _e.mock.On("UpdatePublicIP", ctx, publicIPOCID, privateIPOCID)} +} + +func (_c *OCINetworkService_UpdatePublicIP_Call) Run(run func(ctx context.Context, publicIPOCID string, privateIPOCID string)) *OCINetworkService_UpdatePublicIP_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *OCINetworkService_UpdatePublicIP_Call) Return(_a0 error) *OCINetworkService_UpdatePublicIP_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *OCINetworkService_UpdatePublicIP_Call) RunAndReturn(run func(context.Context, string, string) error) *OCINetworkService_UpdatePublicIP_Call { + _c.Call.Return(run) + return _c +} + +// NewOCINetworkService creates a new instance of OCINetworkService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewOCINetworkService(t interface { + mock.TestingT + Cleanup(func()) +}) *OCINetworkService { + mock := &OCINetworkService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}