├── CNAME ├── _config.yml ├── chart ├── Chart.yaml ├── templates │ ├── secrets.yaml │ ├── clusterrolebinding.yaml │ ├── clusterrole.yaml │ ├── serviceaccount.yaml │ ├── NOTES.txt │ ├── _helpers.tpl │ └── daemonset.yaml └── values.yaml ├── internal ├── address │ ├── azure.go │ ├── assigner.go │ ├── aws.go │ └── oci.go ├── cloud │ ├── gcp_instance.go │ ├── gcp_waiter.go │ ├── aws_instance.go │ ├── gcp_lister.go │ ├── aws_address.go │ ├── aws_lister.go │ ├── oci_instance_service.go │ ├── gcp_address.go │ └── oci_network_service.go ├── types │ ├── node.go │ ├── filters.go │ └── filters_test.go ├── config │ └── config.go ├── node │ ├── tainter.go │ ├── explorer.go │ └── tainter_test.go └── lease │ ├── lock_test.go │ └── lock.go ├── .gitignore ├── sonar-project.properties ├── examples ├── aws │ ├── variables.tf │ ├── .terraform.lock.hcl │ └── eks.tf └── gcp │ ├── variables.tf │ ├── .terraform.lock.hcl │ └── gke.tf ├── LICENSE ├── Dockerfile ├── mocks ├── cloud │ ├── Lister.go │ ├── ZoneWaiter.go │ ├── InstanceGetter.go │ ├── EipLister.go │ ├── Ec2InstanceGetter.go │ ├── OCIInstanceService.go │ ├── WaitCall.go │ ├── EipAssigner.go │ ├── ListCall.go │ ├── AddressManager.go │ └── OCINetworkService.go ├── node │ └── Explorer.go └── address │ ├── Assigner.go │ └── internalAssigner.go ├── makefile ├── .golangci.yaml ├── .github └── workflows │ └── build.yaml ├── go.mod └── cmd └── main_test.go /CNAME: -------------------------------------------------------------------------------- 1 | kubeip.com -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: kubeip 3 | description: A Helm chart for KubeIP 4 | version: 0.1.0 -------------------------------------------------------------------------------- /chart/templates/secrets.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.secrets.create }} 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: kubeip-oci-secret 6 | namespace: {{ .Values.namespaceOverride }} 7 | type: Opaque 8 | data: 9 | oci_config: {{ .Values.secrets.oci_config }} 10 | oci_oci_api_key.pem: {{ .Values.secrets.oci_oci_api_key }} 11 | {{- end }} -------------------------------------------------------------------------------- /internal/address/azure.go: -------------------------------------------------------------------------------- 1 | package address 2 | 3 | import "context" 4 | 5 | type azureAssigner struct { 6 | } 7 | 8 | func (a *azureAssigner) Assign(_ context.Context, _, _ string, _ []string, _ string) (string, error) { 9 | return "", nil 10 | } 11 | 12 | func (a *azureAssigner) Unassign(_ context.Context, _, _ string) error { 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /chart/templates/clusterrolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRoleBinding 4 | metadata: 5 | name: {{ include "kubeip.fullname" . }}-cluster-role-binding 6 | labels: 7 | {{- include "kubeip.labels" . | nindent 4 }} 8 | subjects: 9 | - kind: ServiceAccount 10 | name: {{ include "kubeip.serviceAccountName" . }} 11 | namespace: {{ include "kubeip.namespace" . }} 12 | roleRef: 13 | kind: ClusterRole 14 | name: {{ include "kubeip.fullname" . }}-cluster-role 15 | apiGroup: rbac.authorization.k8s.io 16 | {{- end }} 17 | -------------------------------------------------------------------------------- /internal/cloud/gcp_instance.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import "google.golang.org/api/compute/v1" 4 | 5 | type InstanceGetter interface { 6 | Get(projectID, zone, instance string) (*compute.Instance, error) 7 | } 8 | 9 | type instanceGetter struct { 10 | client *compute.Service 11 | } 12 | 13 | func NewInstanceGetter(client *compute.Service) InstanceGetter { 14 | return &instanceGetter{client: client} 15 | } 16 | 17 | func (g *instanceGetter) Get(projectID, zone, instance string) (*compute.Instance, error) { 18 | return g.client.Instances.Get(projectID, zone, instance).Do() //nolint:wrapcheck 19 | } 20 | -------------------------------------------------------------------------------- /chart/templates/clusterrole.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "kubeip.fullname" . }}-cluster-role 6 | labels: 7 | {{- include "kubeip.labels" . | nindent 4 }} 8 | rules: 9 | - apiGroups: [ "" ] 10 | resources: [ "nodes" ] 11 | {{- if .Values.rbac.allowNodesPatchPermission }} 12 | verbs: [ "get", "patch" ] 13 | {{- else }} 14 | verbs: [ "get" ] 15 | {{- end }} 16 | - apiGroups: [ "coordination.k8s.io" ] 17 | resources: [ "leases" ] 18 | verbs: [ "create", "delete", "get" ] 19 | {{- end }} 20 | -------------------------------------------------------------------------------- /internal/types/node.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | ) 7 | 8 | type CloudProvider string 9 | 10 | const ( 11 | CloudProviderGCP CloudProvider = "gcp" 12 | CloudProviderAWS CloudProvider = "aws" 13 | CloudProviderOCI CloudProvider = "oci" 14 | CloudProviderAzure CloudProvider = "azure" 15 | ) 16 | 17 | type Node struct { 18 | Name string 19 | Instance string 20 | Cloud CloudProvider 21 | Pool string 22 | Region string 23 | Zone string 24 | ExternalIPs []net.IP 25 | InternalIPs []net.IP 26 | } 27 | 28 | // Stringer interface: all fields with name and value 29 | func (n *Node) String() string { 30 | return fmt.Sprintf("%+v", *n) 31 | } 32 | -------------------------------------------------------------------------------- /chart/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "kubeip.serviceAccountName" . }} 6 | namespace: {{ include "kubeip.namespace" . }} 7 | annotations: 8 | {{- if eq .Values.cloudProvider "gcp" }} 9 | iam.gke.io/gcp-service-account: {{ required "A valid .Values.serviceAccount.annotations.gcpServiceAccountEmail entry required when cloudProvider is gcp" .Values.serviceAccount.annotations.gcpServiceAccountEmail }} 10 | {{- else if eq .Values.cloudProvider "aws" }} 11 | eks.amazonaws.com/role-arn: {{ required "A valid .Values.serviceAccount.annotations.awsRoleArn entry required when cloudProvider is aws" .Values.serviceAccount.annotations.awsRoleArn }} 12 | {{- end }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | .bin 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | .run 12 | 13 | # Local environment 14 | .env 15 | .in 16 | .out 17 | 18 | # Local .terraform directories 19 | **/.terraform/* 20 | 21 | # Sonar Scanner 22 | .scannerwork/ 23 | 24 | # .tfstate files 25 | *.tfstate 26 | *.tfstate.* 27 | 28 | 29 | # Output of the go coverage tool, specifically when used with LiteIDE 30 | *.out 31 | 32 | # IDE support files 33 | .idea 34 | .vscode 35 | 36 | # MacOS file system metadata 37 | .DS_Store 38 | 39 | # local credentials 40 | .credentials 41 | 42 | # Keep out some temporary example code and temprary file 43 | example_code/ 44 | older/ 45 | temp/ 46 | 47 | # Dependency directories (remove the comment below to include it) 48 | vendor/ 49 | 50 | tmp/ -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=doitintl_kubeip 2 | sonar.organization=doitintl 3 | sonar.host.url=https://sonarcloud.io 4 | # This is the name and version displayed in the SonarCloud UI. 5 | sonar.projectName=kubeip 6 | #sonar.projectVersion=1.0 7 | # Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. 8 | # This is where the source code is located. 9 | sonar.sources=. 10 | sonar.exclusions=**/*_test.go,mocks/**/*.go,**/mock_*.go 11 | # This is where the tests are located. 12 | sonar.tests=. 13 | sonar.test.inclusions=**/*_test.go 14 | # Test coverage report 15 | sonar.go.coverage.reportPaths=coverage.out 16 | # Test report json file 17 | sonar.go.tests.reportPaths=test-report.out 18 | # golangci-lint report json file 19 | sonar.go.golangci-lint.reportPaths=golangci-lint.out 20 | # Encoding of the source code. Default is default system encoding 21 | sonar.sourceEncoding=UTF-8 -------------------------------------------------------------------------------- /examples/aws/variables.tf: -------------------------------------------------------------------------------- 1 | variable "region" { 2 | type = string 3 | default = "us-west-2" 4 | } 5 | 6 | variable "availability_zones" { 7 | type = list(string) 8 | default = ["us-west-2a", "us-west-2b", "us-west-2c"] 9 | } 10 | 11 | variable "cluster_name" { 12 | type = string 13 | default = "kubeip-demo" 14 | } 15 | 16 | variable "vpc_name" { 17 | type = string 18 | default = "kubeip-demo" 19 | } 20 | 21 | variable "vpc_cidr" { 22 | type = string 23 | default = "10.0.0.0/16" 24 | } 25 | 26 | variable "private_cidr_ranges" { 27 | type = list(string) 28 | default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] 29 | } 30 | 31 | variable "public_cidr_ranges" { 32 | type = list(string) 33 | default = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"] 34 | } 35 | 36 | variable "kubernetes_version" { 37 | type = string 38 | default = "1.28" 39 | } 40 | 41 | variable "kubeip_version" { 42 | type = string 43 | default = "latest" 44 | } -------------------------------------------------------------------------------- /internal/cloud/gcp_waiter.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "context" 5 | 6 | "google.golang.org/api/compute/v1" 7 | ) 8 | 9 | type WaitCall interface { 10 | Context(ctx context.Context) WaitCall 11 | Do() (*compute.Operation, error) 12 | } 13 | 14 | type ZoneWaiter interface { 15 | Wait(projectID, region, operationName string) WaitCall 16 | } 17 | 18 | type zoneWaiter struct { 19 | client *compute.Service 20 | } 21 | 22 | type zoneWaitCall struct { 23 | call *compute.ZoneOperationsWaitCall 24 | } 25 | 26 | func NewZoneWaiter(client *compute.Service) ZoneWaiter { 27 | return &zoneWaiter{client: client} 28 | } 29 | 30 | func (w *zoneWaiter) Wait(projectID, region, operationName string) WaitCall { 31 | return &zoneWaitCall{w.client.ZoneOperations.Wait(projectID, region, operationName)} 32 | } 33 | 34 | func (c *zoneWaitCall) Context(ctx context.Context) WaitCall { 35 | return &zoneWaitCall{c.call.Context(ctx)} 36 | } 37 | 38 | func (c *zoneWaitCall) Do() (*compute.Operation, error) { 39 | return c.call.Do() //nolint:wrapcheck 40 | } 41 | -------------------------------------------------------------------------------- /examples/gcp/variables.tf: -------------------------------------------------------------------------------- 1 | variable "project_id" { 2 | type = string 3 | } 4 | 5 | variable "region" { 6 | type = string 7 | default = "us-central1" 8 | } 9 | 10 | variable "cluster_name" { 11 | type = string 12 | default = "kubeip-demo" 13 | } 14 | 15 | variable "vpc_name" { 16 | type = string 17 | default = "kubeip-demo" 18 | } 19 | 20 | variable "subnet_range" { 21 | type = string 22 | default = "10.128.0.0/20" 23 | } 24 | 25 | variable "pods_range" { 26 | type = string 27 | default = "10.128.64.0/18" 28 | } 29 | 30 | variable "pods_range_name" { 31 | type = string 32 | default = "pods-range" 33 | } 34 | 35 | variable "services_range_name" { 36 | type = string 37 | default = "services-range" 38 | } 39 | 40 | variable "services_range" { 41 | type = string 42 | default = "10.128.32.0/20" 43 | } 44 | 45 | variable "machine_type" { 46 | type = string 47 | default = "e2-medium" 48 | } 49 | 50 | variable "ipv6_support" { 51 | type = bool 52 | default = false 53 | } 54 | 55 | variable "kubeip_version" { 56 | type = string 57 | default = "latest" 58 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 DoiT International 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /internal/cloud/aws_instance.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/service/ec2" 7 | "github.com/aws/aws-sdk-go-v2/service/ec2/types" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type Ec2InstanceGetter interface { 12 | Get(ctx context.Context, instanceID, region string) (*types.Instance, error) 13 | } 14 | 15 | type ec2InstanceGetter struct { 16 | client *ec2.Client 17 | } 18 | 19 | func NewEc2InstanceGetter(client *ec2.Client) Ec2InstanceGetter { 20 | return &ec2InstanceGetter{client: client} 21 | } 22 | 23 | func (g *ec2InstanceGetter) Get(ctx context.Context, instanceID, _ string) (*types.Instance, error) { 24 | input := &ec2.DescribeInstancesInput{ 25 | InstanceIds: []string{ 26 | instanceID, 27 | }, 28 | } 29 | 30 | resp, err := g.client.DescribeInstances(ctx, input) 31 | if err != nil { 32 | return nil, errors.Wrap(err, "failed to describe instances, %v") 33 | } 34 | 35 | if len(resp.Reservations) == 0 || len(resp.Reservations[0].Instances) == 0 { 36 | return nil, errors.Wrap(err, "no instances found for the given id") 37 | } 38 | 39 | return &resp.Reservations[0].Instances[0], nil 40 | } 41 | -------------------------------------------------------------------------------- /internal/cloud/gcp_lister.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import "google.golang.org/api/compute/v1" 4 | 5 | type ListCall interface { 6 | Filter(filter string) ListCall 7 | OrderBy(orderBy string) ListCall 8 | PageToken(pageToken string) ListCall 9 | Do() (*compute.AddressList, error) 10 | } 11 | 12 | type Lister interface { 13 | List(projectID, region string) ListCall 14 | } 15 | 16 | func NewLister(client *compute.Service) Lister { 17 | return &gcpLister{client: client} 18 | } 19 | 20 | type gcpLister struct { 21 | client *compute.Service 22 | } 23 | 24 | type gcpListCall struct { 25 | call *compute.AddressesListCall 26 | } 27 | 28 | func (l *gcpLister) List(projectID, region string) ListCall { 29 | return &gcpListCall{l.client.Addresses.List(projectID, region)} 30 | } 31 | 32 | func (c *gcpListCall) Filter(filter string) ListCall { 33 | return &gcpListCall{c.call.Filter(filter)} 34 | } 35 | 36 | func (c *gcpListCall) OrderBy(orderBy string) ListCall { 37 | return &gcpListCall{c.call.OrderBy(orderBy)} 38 | } 39 | 40 | func (c *gcpListCall) PageToken(pageToken string) ListCall { 41 | return &gcpListCall{c.call.PageToken(pageToken)} 42 | } 43 | 44 | func (c *gcpListCall) Do() (*compute.AddressList, error) { 45 | return c.call.Do() //nolint:wrapcheck 46 | } 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder 2 | 3 | # add CA certificates and TZ for local time 4 | RUN apk --update add ca-certificates tzdata make git 5 | 6 | # create a working directory 7 | WORKDIR /app 8 | 9 | # Retrieve application dependencies. 10 | # This allows the container build to reuse cached dependencies. 11 | # Expecting to copy go.mod and if present go.sum. 12 | COPY go.* ./ 13 | RUN go mod download 14 | 15 | # Copy local code to the container image. 16 | COPY . ./ 17 | 18 | # get version, commit and branch from build args 19 | ARG VERSION 20 | ARG COMMIT 21 | ARG BRANCH 22 | ARG TARGETOS 23 | ARG TARGETARCH 24 | 25 | # Build the binary with make (using the version, commit and branch) 26 | RUN make build VERSION=${VERSION} COMMIT=${COMMIT} BRANCH=${BRANCH} TARGETOS=${TARGETOS} TARGETARCH=${TARGETARCH} 27 | 28 | # final image 29 | FROM scratch 30 | # copy CA certificates 31 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 32 | # copy timezone settings 33 | COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo 34 | # copy the binary to the production image from the builder stage 35 | COPY --from=builder /app/.bin/kubeip-agent /kubeip-agent 36 | 37 | USER 1001 38 | 39 | ENTRYPOINT ["/kubeip-agent"] 40 | CMD ["run"] -------------------------------------------------------------------------------- /internal/address/assigner.go: -------------------------------------------------------------------------------- 1 | package address 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/doitintl/kubeip/internal/config" 8 | "github.com/doitintl/kubeip/internal/types" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var ( 13 | ErrUnknownCloudProvider = errors.New("unknown cloud provider") 14 | ErrStaticIPAlreadyAssigned = errors.New("static public IP already assigned") 15 | ErrNoStaticIPAssigned = errors.New("no static public IP assigned") 16 | ) 17 | 18 | type Assigner interface { 19 | Assign(ctx context.Context, instanceID, zone string, filter []string, orderBy string) (string, error) 20 | Unassign(ctx context.Context, instanceID, zone string) error 21 | } 22 | 23 | func NewAssigner(ctx context.Context, logger *logrus.Entry, provider types.CloudProvider, cfg *config.Config) (Assigner, error) { 24 | if provider == types.CloudProviderAWS { 25 | return NewAwsAssigner(ctx, logger, cfg.Region) 26 | } else if provider == types.CloudProviderAzure { 27 | return &azureAssigner{}, nil 28 | } else if provider == types.CloudProviderGCP { 29 | return NewGCPAssigner(ctx, logger, cfg.Project, cfg.Region, cfg.IPv6) 30 | } else if provider == types.CloudProviderOCI { 31 | return NewOCIAssigner(ctx, logger, cfg) 32 | } 33 | return nil, ErrUnknownCloudProvider 34 | } 35 | -------------------------------------------------------------------------------- /chart/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | {{- if .Release.IsInstall }} 2 | 🎉 KubeIP v2 Deployment Successful! 🎉 3 | 4 | Thank you for installing KubeIP v2, ensuring that your Kubernetes nodes are now equipped with static public IP addresses for improved connectivity and reliability. 5 | 6 | Next Steps: 7 | 8 | 1. Verify the Operation: Ensure that KubeIP is running successfully on all desired nodes. You can check the status of the DaemonSet by running: 9 | 10 | $ kubectl get daemonset kubeip -n kube-system 11 | 12 | 2. Check IP Assignment: Ensure that static public IPs are assigned to your nodes. Run the following command to see the assigned IPs: 13 | 14 | $ kubectl get nodes -o wide 15 | 16 | 3. Review Logs (Optional): If you want to delve deeper or troubleshoot, you can review the logs of the KubeIP pods: 17 | 18 | $ kubectl logs -l app=kubeip -n kube-system 19 | 20 | 4. Update Your Firewall Rules: If you have specific firewall rules or IP whitelists, ensure they are updated to include the static IPs assigned to your nodes. 21 | 22 | 5. Documentation and Support: For more information on configuration options, troubleshooting, and usage, please visit the [official KubeIP repository](https://github.com/doitintl/kubeip). 23 | 24 | 6. Feedback and Contributions: Your feedback is valuable! If you encounter any issues, or if you have suggestions for improvements, please feel free to open an issue or contribute to the project on GitHub. 25 | 26 | Enjoy the enhanced stability and connectivity that KubeIP brings to your Kubernetes cluster! 🚀 27 | {{- end }} 28 | -------------------------------------------------------------------------------- /internal/cloud/aws_address.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/service/ec2" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type EipAssigner interface { 12 | Assign(ctx context.Context, networkInterfaceID, allocationID string) error 13 | Unassign(ctx context.Context, associationID string) error 14 | } 15 | 16 | type eipAssigner struct { 17 | client *ec2.Client 18 | } 19 | 20 | func NewEipAssigner(client *ec2.Client) EipAssigner { 21 | return &eipAssigner{client: client} 22 | } 23 | 24 | func (a *eipAssigner) Assign(ctx context.Context, networkInterfaceID, allocationID string) error { 25 | // associate elastic IP with the instance 26 | input := &ec2.AssociateAddressInput{ 27 | AllocationId: &allocationID, 28 | NetworkInterfaceId: &networkInterfaceID, 29 | AllowReassociation: aws.Bool(false), // do not allow reassociation of the elastic IP 30 | } 31 | 32 | _, err := a.client.AssociateAddress(ctx, input) 33 | if err != nil { 34 | return errors.Wrap(err, "failed to associate elastic IP with the instance") 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (a *eipAssigner) Unassign(ctx context.Context, associationID string) error { 41 | // disassociate elastic IP from the instance 42 | input := &ec2.DisassociateAddressInput{ 43 | AssociationId: &associationID, 44 | } 45 | 46 | _, err := a.client.DisassociateAddress(ctx, input) 47 | if err != nil { 48 | return errors.Wrap(err, "failed to disassociate elastic IP from the instance") 49 | } 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/cloud/aws_lister.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/service/ec2" 7 | "github.com/aws/aws-sdk-go-v2/service/ec2/types" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type EipLister interface { 12 | List(ctx context.Context, filter map[string][]string, inUse bool) ([]types.Address, error) 13 | } 14 | 15 | type eipLister struct { 16 | client *ec2.Client 17 | } 18 | 19 | func NewEipLister(client *ec2.Client) EipLister { 20 | return &eipLister{client: client} 21 | } 22 | 23 | func (l *eipLister) List(ctx context.Context, filter map[string][]string, inUse bool) ([]types.Address, error) { 24 | // create filter for DescribeAddressesInput 25 | filters := make([]types.Filter, 0, len(filter)+1) 26 | for k, v := range filter { 27 | key := k 28 | filters = append(filters, types.Filter{ 29 | Name: &key, 30 | Values: v, 31 | }) 32 | } 33 | 34 | // list all elastic IPs in the region matching the filter 35 | input := &ec2.DescribeAddressesInput{ 36 | Filters: filters, 37 | } 38 | list, err := l.client.DescribeAddresses(ctx, input) 39 | if err != nil { 40 | return nil, errors.Wrap(err, "failed to list elastic IPs") 41 | } 42 | 43 | filtered := make([]types.Address, 0, len(list.Addresses)) 44 | // API does not support filtering by association ID equal to nil 45 | // filter addresses based on whether they are in use or not 46 | for _, address := range list.Addresses { 47 | if (inUse && address.AssociationId != nil) || (!inUse && address.AssociationId == nil) { 48 | filtered = append(filtered, address) 49 | } 50 | } 51 | 52 | return filtered, nil 53 | } 54 | -------------------------------------------------------------------------------- /chart/values.yaml: -------------------------------------------------------------------------------- 1 | # The cloud provider where your Kubernetes cluster is running. 2 | # This value determines the appropriate annotations for the Service Account. 3 | # Currently acceptable values are 'gcp' or 'aws' or 'oci'. 4 | cloudProvider: gcp 5 | 6 | # The namespace where the kubeip-agent will be deployed. 7 | namespaceOverride: kube-system 8 | 9 | # Configuration settings for the container image. 10 | image: 11 | repository: doitintl/kubeip-agent 12 | tag: latest 13 | 14 | # Configuration for the Kubernetes Service Account. 15 | serviceAccount: 16 | create: true 17 | name: kubeip-service-account 18 | annotations: 19 | gcpServiceAccountEmail: kubeip-service-account@workload-id-117715.iam.gserviceaccount.com 20 | # annotations: 21 | # awsRoleArn: "your-aws-role-arn" 22 | # gcpServiceAccountEmail: "your-google-service-account-email" 23 | 24 | 25 | # Role-Based Access Control (RBAC) configuration. 26 | rbac: 27 | create: true 28 | allowNodesPatchPermission: false 29 | 30 | # Secret configuration for oci users. 31 | secrets: 32 | create: true 33 | oci_config: "" # base64 encoded oci config file 34 | oci_oci_api_key: "" # base64 encoded oci api key file 35 | 36 | # DaemonSet configuration. 37 | daemonSet: 38 | terminationGracePeriodSeconds: 30 39 | priorityClassName: system-node-critical 40 | nodeSelector: 41 | nodegroup: public 42 | kubeip: use 43 | env: 44 | FILTER: labels.kubeip=reserved;labels.environment=demo 45 | TAINT_KEY: "" 46 | LOG_LEVEL: debug 47 | LOG_JSON: true 48 | resources: 49 | requests: 50 | cpu: 100m 51 | memory: 64Mi 52 | limits: 53 | cpu: 100m 54 | memory: 128Mi 55 | -------------------------------------------------------------------------------- /internal/cloud/oci_instance_service.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/oracle/oci-go-sdk/v65/common" 7 | "github.com/oracle/oci-go-sdk/v65/core" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // OCIInstanceService is the interface for all instance related operations in OCI. 12 | type OCIInstanceService interface { 13 | ListVnicAttachments(ctx context.Context, compartmentOCID, instanceOCID string) ([]core.VnicAttachment, error) 14 | } 15 | 16 | // ociInstanceService is the implementation of OCIInstanceService. 17 | type ociInstanceService struct { 18 | client core.ComputeClient 19 | } 20 | 21 | // NewOCIInstanceService creates a new instance of OCIInstanceService. 22 | func NewOCIInstanceService() (OCIInstanceService, error) { 23 | client, err := core.NewComputeClientWithConfigurationProvider(common.DefaultConfigProvider()) 24 | if err != nil { 25 | return nil, errors.Wrap(err, "failed to create OCI Compute client") 26 | } 27 | 28 | return &ociInstanceService{client: client}, nil 29 | } 30 | 31 | // ListVnicAttachments lists all VNIC attachments for the given compartment and instance OCID. 32 | func (svc *ociInstanceService) ListVnicAttachments(ctx context.Context, compartmentOCID, instanceOCID string) ([]core.VnicAttachment, error) { 33 | request := core.ListVnicAttachmentsRequest{ 34 | CompartmentId: common.String(compartmentOCID), 35 | InstanceId: common.String(instanceOCID), 36 | } 37 | response, err := svc.client.ListVnicAttachments(ctx, request) 38 | if err != nil { 39 | return nil, errors.Wrap(err, "error while listing VNIC attachments") 40 | } 41 | 42 | if response.Items == nil { 43 | return nil, errors.Errorf("no VNIC attachments found, compartmentOCID: %s, instanceOCID: %s", compartmentOCID, instanceOCID) 44 | } 45 | 46 | return response.Items, nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | type Config struct { 10 | // KubeConfigPath is the path to the kubeconfig file 11 | KubeConfigPath string `json:"kubeconfig"` 12 | // NodeName is the name of the Kubernetes node 13 | NodeName string `json:"node-name"` 14 | // Project is the name of the GCP project or the AWS account ID or the OCI compartment OCID 15 | Project string `json:"project"` 16 | // Region is the name of the GCP region or the AWS region or the OCI region 17 | Region string `json:"region"` 18 | // IPv6 support 19 | IPv6 bool `json:"ipv6"` 20 | // DevelopMode mode 21 | DevelopMode bool `json:"develop-mode"` 22 | // Filter is the filter for the IP addresses 23 | Filter []string `json:"filter"` 24 | // OrderBy is the order by for the IP addresses 25 | OrderBy string `json:"order-by"` 26 | // Retry interval 27 | RetryInterval time.Duration `json:"retry-interval"` 28 | // Retry attempts 29 | RetryAttempts int `json:"retry-attempts"` 30 | // ReleaseOnExit releases the IP address on exit 31 | ReleaseOnExit bool `json:"release-on-exit"` 32 | // LeaseDuration is the duration of the kubernetes lease 33 | LeaseDuration int `json:"lease-duration"` 34 | // LeaseNamespace is the namespace of the kubernetes lease 35 | LeaseNamespace string `json:"lease-namespace"` 36 | // TaintKey is the taint key to remove from the node once the IP address is assigned 37 | TaintKey string `json:"taint-key"` 38 | } 39 | 40 | func NewConfig(c *cli.Context) *Config { 41 | var cfg Config 42 | cfg.KubeConfigPath = c.String("kubeconfig") 43 | cfg.NodeName = c.String("node-name") 44 | cfg.DevelopMode = c.Bool("develop-mode") 45 | cfg.RetryInterval = c.Duration("retry-interval") 46 | cfg.RetryAttempts = c.Int("retry-attempts") 47 | cfg.Filter = c.StringSlice("filter") 48 | cfg.OrderBy = c.String("order-by") 49 | cfg.Project = c.String("project") 50 | cfg.Region = c.String("region") 51 | cfg.IPv6 = c.Bool("ipv6") 52 | cfg.ReleaseOnExit = c.Bool("release-on-exit") 53 | cfg.LeaseDuration = c.Int("lease-duration") 54 | cfg.LeaseNamespace = c.String("lease-namespace") 55 | cfg.TaintKey = c.String("taint-key") 56 | return &cfg 57 | } 58 | -------------------------------------------------------------------------------- /internal/types/filters.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // OCIFilters is a struct that holds the filters for the OCI. 10 | type OCIFilters struct { 11 | FreeformTags map[string]string 12 | DefinedTags map[string]map[string]interface{} 13 | } 14 | 15 | // CheckFreeformTagFilter checks if the target contains all the filter keys and values. 16 | func (f *OCIFilters) CheckFreeformTagFilter(target map[string]string) bool { 17 | // If the filter is nil, return true, since there is no filter to apply 18 | if f.FreeformTags == nil { 19 | return true 20 | } 21 | 22 | // If the target is nil, return false, since filter cannot be applied 23 | if target == nil { 24 | return false 25 | } 26 | 27 | // Loop through the filter map and check if the target map contains all the filter keys and values 28 | for key, value := range f.FreeformTags { 29 | if val, ok := target[key]; !ok || val != value { 30 | return false 31 | } 32 | } 33 | return true 34 | } 35 | 36 | // ParseFreeformTagFilter parses the filter string for freeform tags. 37 | // Filter should be in following format: 38 | // - "freeformTags.key=value" 39 | func ParseFreeformTagFilter(filter string) (string, string, error) { 40 | f := filter 41 | if strings.HasPrefix(f, "freeformTags.") { 42 | f = strings.TrimPrefix(f, "freeformTags.") 43 | if split := strings.Split(f, "="); len(split) == 2 { //nolint:gomnd 44 | return split[0], split[1], nil 45 | } 46 | } 47 | 48 | return "", "", errors.New("invalid filter format for freeform tags, should be in format freeformTags.key=value, found: " + filter) 49 | } 50 | 51 | // ParseDefinedTagFilter parses the filter string for defined tags. 52 | // Filter should be in following format: 53 | // - "definedTags.Namespace.key=value" 54 | // 55 | // TODO: Add filter support for DefinedTags 56 | func ParseDefinedTagFilter(_ string) (string, string, string, error) { 57 | return "", "", "", nil 58 | } 59 | 60 | // CheckDefinedTagFilter checks if the target contains all the filter keys and values. 61 | // TODO: Add filter support for DefinedTags 62 | func (f *OCIFilters) CheckDefinedTagFilter(_ map[string]map[string]interface{}) bool { 63 | return true 64 | } 65 | -------------------------------------------------------------------------------- /internal/node/tainter.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/doitintl/kubeip/internal/types" 9 | "github.com/pkg/errors" 10 | v1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | typesv1 "k8s.io/apimachinery/pkg/types" 13 | "k8s.io/client-go/kubernetes" 14 | ) 15 | 16 | type Tainter interface { 17 | RemoveTaintKey(ctx context.Context, node *types.Node, taintKey string) (bool, error) 18 | } 19 | 20 | type tainter struct { 21 | client kubernetes.Interface 22 | } 23 | 24 | func deleteTaintsByKey(taints []v1.Taint, taintKey string) ([]v1.Taint, bool) { 25 | newTaints := []v1.Taint{} 26 | didDelete := false 27 | 28 | for i := range taints { 29 | if taintKey == taints[i].Key { 30 | didDelete = true 31 | continue 32 | } 33 | newTaints = append(newTaints, taints[i]) 34 | } 35 | 36 | return newTaints, didDelete 37 | } 38 | 39 | func NewTainter(client kubernetes.Interface) Tainter { 40 | return &tainter{ 41 | client: client, 42 | } 43 | } 44 | 45 | func (t *tainter) RemoveTaintKey(ctx context.Context, node *types.Node, taintKey string) (bool, error) { 46 | // get node object from API server 47 | n, err := t.client.CoreV1().Nodes().Get(ctx, node.Name, metav1.GetOptions{}) 48 | if err != nil { 49 | return false, errors.Wrap(err, "failed to get kubernetes node") 50 | } 51 | 52 | // Remove taint from the node representation 53 | newTaints, didDelete := deleteTaintsByKey(n.Spec.Taints, taintKey) 54 | if !didDelete { 55 | return false, nil 56 | } 57 | 58 | // Marshal the remaining taints of the node into json format for patching. 59 | // The remaining taints may be empty, and that will result in an empty json array "[]" 60 | newTaintsMarshaled, err := json.Marshal(newTaints) 61 | if err != nil { 62 | return false, errors.Wrap(err, "failed to marshal new taints") 63 | } 64 | 65 | // Patch the node with only the remaining taints 66 | patch := fmt.Sprintf(`{"spec":{"taints":%v}}`, string(newTaintsMarshaled)) 67 | _, err = t.client.CoreV1().Nodes().Patch(ctx, node.Name, typesv1.MergePatchType, []byte(patch), metav1.PatchOptions{}) 68 | if err != nil { 69 | return false, errors.Wrap(err, "failed to patch node taints") 70 | } 71 | 72 | return true, nil 73 | } 74 | -------------------------------------------------------------------------------- /mocks/cloud/Lister.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | cloud "github.com/doitintl/kubeip/internal/cloud" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // Lister is an autogenerated mock type for the Lister type 11 | type Lister struct { 12 | mock.Mock 13 | } 14 | 15 | type Lister_Expecter struct { 16 | mock *mock.Mock 17 | } 18 | 19 | func (_m *Lister) EXPECT() *Lister_Expecter { 20 | return &Lister_Expecter{mock: &_m.Mock} 21 | } 22 | 23 | // List provides a mock function with given fields: projectID, region 24 | func (_m *Lister) List(projectID string, region string) cloud.ListCall { 25 | ret := _m.Called(projectID, region) 26 | 27 | var r0 cloud.ListCall 28 | if rf, ok := ret.Get(0).(func(string, string) cloud.ListCall); ok { 29 | r0 = rf(projectID, region) 30 | } else { 31 | if ret.Get(0) != nil { 32 | r0 = ret.Get(0).(cloud.ListCall) 33 | } 34 | } 35 | 36 | return r0 37 | } 38 | 39 | // Lister_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List' 40 | type Lister_List_Call struct { 41 | *mock.Call 42 | } 43 | 44 | // List is a helper method to define mock.On call 45 | // - projectID string 46 | // - region string 47 | func (_e *Lister_Expecter) List(projectID interface{}, region interface{}) *Lister_List_Call { 48 | return &Lister_List_Call{Call: _e.mock.On("List", projectID, region)} 49 | } 50 | 51 | func (_c *Lister_List_Call) Run(run func(projectID string, region string)) *Lister_List_Call { 52 | _c.Call.Run(func(args mock.Arguments) { 53 | run(args[0].(string), args[1].(string)) 54 | }) 55 | return _c 56 | } 57 | 58 | func (_c *Lister_List_Call) Return(_a0 cloud.ListCall) *Lister_List_Call { 59 | _c.Call.Return(_a0) 60 | return _c 61 | } 62 | 63 | func (_c *Lister_List_Call) RunAndReturn(run func(string, string) cloud.ListCall) *Lister_List_Call { 64 | _c.Call.Return(run) 65 | return _c 66 | } 67 | 68 | // NewLister creates a new instance of Lister. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 69 | // The first argument is typically a *testing.T value. 70 | func NewLister(t interface { 71 | mock.TestingT 72 | Cleanup(func()) 73 | }) *Lister { 74 | mock := &Lister{} 75 | mock.Mock.Test(t) 76 | 77 | t.Cleanup(func() { mock.AssertExpectations(t) }) 78 | 79 | return mock 80 | } 81 | -------------------------------------------------------------------------------- /internal/cloud/gcp_address.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "google.golang.org/api/compute/v1" 5 | ) 6 | 7 | const ( 8 | ipv4Only = "IPV4_ONLY" 9 | ipv4ipv6 = "IPV4_IPV6" 10 | ) 11 | 12 | type AddressManager interface { 13 | AddAccessConfig(project string, zone string, instance string, networkInterface string, fingerprint string, accessconfig *compute.AccessConfig) (*compute.Operation, error) 14 | DeleteAccessConfig(project string, zone string, instance string, accessConfig string, networkInterface string, fingerprint string) (*compute.Operation, error) 15 | GetAddress(project, region, name string) (*compute.Address, error) 16 | } 17 | 18 | type addressManager struct { 19 | client *compute.Service 20 | ipv6 bool 21 | } 22 | 23 | func NewAddressManager(client *compute.Service, ipv6 bool) AddressManager { 24 | return &addressManager{client: client, ipv6: ipv6} 25 | } 26 | 27 | func (m *addressManager) AddAccessConfig(project, zone, instance, networkInterface, fingerprint string, accessconfig *compute.AccessConfig) (*compute.Operation, error) { 28 | if m.ipv6 { 29 | // Add the IPv6 address configuration by updating the network interface with the IPv6 stack type and Ipv6AccessConfigs struct 30 | return m.client.Instances.UpdateNetworkInterface(project, zone, instance, networkInterface, &compute.NetworkInterface{ //nolint:wrapcheck 31 | Fingerprint: fingerprint, // Required to update network interface 32 | StackType: ipv4ipv6, 33 | Ipv6AccessConfigs: []*compute.AccessConfig{ 34 | accessconfig, 35 | }, 36 | }).Do() 37 | } 38 | return m.client.Instances.AddAccessConfig(project, zone, instance, networkInterface, accessconfig).Do() //nolint:wrapcheck 39 | } 40 | 41 | func (m *addressManager) DeleteAccessConfig(project, zone, instance, accessConfig, networkInterface, fingerprint string) (*compute.Operation, error) { 42 | if m.ipv6 { 43 | // Remove the existing IPv6 address configuration by updating the network interface with the IPv4 only stack type. 44 | return m.client.Instances.UpdateNetworkInterface(project, zone, instance, networkInterface, &compute.NetworkInterface{ //nolint:wrapcheck 45 | Fingerprint: fingerprint, // Required to update network interface 46 | StackType: ipv4Only, 47 | }).Do() 48 | } 49 | return m.client.Instances.DeleteAccessConfig(project, zone, instance, accessConfig, networkInterface).Do() //nolint:wrapcheck 50 | } 51 | 52 | func (m *addressManager) GetAddress(project, region, name string) (*compute.Address, error) { 53 | return m.client.Addresses.Get(project, region, name).Do() //nolint:wrapcheck 54 | } 55 | -------------------------------------------------------------------------------- /chart/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "kubeip.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 7 | {{- end }} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | */}} 12 | {{- define "kubeip.fullname" -}} 13 | {{- if .Values.fullnameOverride }} 14 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 15 | {{- else }} 16 | {{- $name := default .Chart.Name .Values.nameOverride }} 17 | {{- if contains $name .Release.Name }} 18 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 19 | {{- else }} 20 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 21 | {{- end }} 22 | {{- end }} 23 | {{- end }} 24 | 25 | {{/* 26 | Create chart name and version as used by the chart label. 27 | */}} 28 | {{- define "kubeip.chart" -}} 29 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 30 | {{- end }} 31 | 32 | {{/* 33 | Common labels 34 | */}} 35 | {{- define "kubeip.labels" -}} 36 | helm.sh/chart: {{ include "kubeip.chart" . }} 37 | {{ include "kubeip.selectorLabels" . }} 38 | {{- with .Chart.AppVersion }} 39 | app.kubernetes.io/version: {{ . | quote }} 40 | {{- end }} 41 | app.kubernetes.io/managed-by: {{ .Release.Service }} 42 | {{- end }} 43 | 44 | {{/* 45 | Selector labels 46 | */}} 47 | {{- define "kubeip.selectorLabels" -}} 48 | app.kubernetes.io/name: {{ include "kubeip.name" . }} 49 | app.kubernetes.io/instance: {{ .Release.Name }} 50 | {{- end }} 51 | 52 | {{/* 53 | Create the name of the service account to use 54 | */}} 55 | {{- define "kubeip.serviceAccountName" -}} 56 | {{- if .Values.serviceAccount.create }} 57 | {{- default (include "kubeip.fullname" .) .Values.serviceAccount.name }} 58 | {{- else }} 59 | {{- default "default" .Values.serviceAccount.name }} 60 | {{- end }} 61 | {{- end }} 62 | 63 | {{/* 64 | Define Ingress apiVersion 65 | */}} 66 | {{- define "kubeip.ingress.apiVersion" -}} 67 | {{- printf "networking.k8s.io/v1" }} 68 | {{- end }} 69 | 70 | {{/* 71 | Define Pdb apiVersion 72 | */}} 73 | {{- define "kubeip.pdb.apiVersion" -}} 74 | {{- if $.Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }} 75 | {{- printf "policy/v1" }} 76 | {{- else }} 77 | {{- printf "policy/v1beta1" }} 78 | {{- end }} 79 | {{- end }} 80 | 81 | {{/* 82 | Allow overriding kubeip namespace 83 | */}} 84 | {{- define "kubeip.namespace" -}} 85 | {{- if .Values.namespaceOverride -}} 86 | {{- .Values.namespaceOverride -}} 87 | {{- else -}} 88 | {{- .Release.Namespace -}} 89 | {{- end -}} 90 | {{- end -}} 91 | -------------------------------------------------------------------------------- /mocks/cloud/ZoneWaiter.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | cloud "github.com/doitintl/kubeip/internal/cloud" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // ZoneWaiter is an autogenerated mock type for the ZoneWaiter type 11 | type ZoneWaiter struct { 12 | mock.Mock 13 | } 14 | 15 | type ZoneWaiter_Expecter struct { 16 | mock *mock.Mock 17 | } 18 | 19 | func (_m *ZoneWaiter) EXPECT() *ZoneWaiter_Expecter { 20 | return &ZoneWaiter_Expecter{mock: &_m.Mock} 21 | } 22 | 23 | // Wait provides a mock function with given fields: projectID, region, operationName 24 | func (_m *ZoneWaiter) Wait(projectID string, region string, operationName string) cloud.WaitCall { 25 | ret := _m.Called(projectID, region, operationName) 26 | 27 | var r0 cloud.WaitCall 28 | if rf, ok := ret.Get(0).(func(string, string, string) cloud.WaitCall); ok { 29 | r0 = rf(projectID, region, operationName) 30 | } else { 31 | if ret.Get(0) != nil { 32 | r0 = ret.Get(0).(cloud.WaitCall) 33 | } 34 | } 35 | 36 | return r0 37 | } 38 | 39 | // ZoneWaiter_Wait_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Wait' 40 | type ZoneWaiter_Wait_Call struct { 41 | *mock.Call 42 | } 43 | 44 | // Wait is a helper method to define mock.On call 45 | // - projectID string 46 | // - region string 47 | // - operationName string 48 | func (_e *ZoneWaiter_Expecter) Wait(projectID interface{}, region interface{}, operationName interface{}) *ZoneWaiter_Wait_Call { 49 | return &ZoneWaiter_Wait_Call{Call: _e.mock.On("Wait", projectID, region, operationName)} 50 | } 51 | 52 | func (_c *ZoneWaiter_Wait_Call) Run(run func(projectID string, region string, operationName string)) *ZoneWaiter_Wait_Call { 53 | _c.Call.Run(func(args mock.Arguments) { 54 | run(args[0].(string), args[1].(string), args[2].(string)) 55 | }) 56 | return _c 57 | } 58 | 59 | func (_c *ZoneWaiter_Wait_Call) Return(_a0 cloud.WaitCall) *ZoneWaiter_Wait_Call { 60 | _c.Call.Return(_a0) 61 | return _c 62 | } 63 | 64 | func (_c *ZoneWaiter_Wait_Call) RunAndReturn(run func(string, string, string) cloud.WaitCall) *ZoneWaiter_Wait_Call { 65 | _c.Call.Return(run) 66 | return _c 67 | } 68 | 69 | // NewZoneWaiter creates a new instance of ZoneWaiter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 70 | // The first argument is typically a *testing.T value. 71 | func NewZoneWaiter(t interface { 72 | mock.TestingT 73 | Cleanup(func()) 74 | }) *ZoneWaiter { 75 | mock := &ZoneWaiter{} 76 | mock.Mock.Test(t) 77 | 78 | t.Cleanup(func() { mock.AssertExpectations(t) }) 79 | 80 | return mock 81 | } 82 | -------------------------------------------------------------------------------- /chart/templates/daemonset.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: DaemonSet 3 | metadata: 4 | name: {{ include "kubeip.fullname" . }} 5 | labels: 6 | {{- include "kubeip.labels" . | nindent 4 }} 7 | spec: 8 | selector: 9 | matchLabels: 10 | app.kubernetes.io/name: {{ include "kubeip.name" . }} 11 | updateStrategy: 12 | type: RollingUpdate 13 | rollingUpdate: 14 | maxUnavailable: 1 15 | template: 16 | metadata: 17 | labels: 18 | app.kubernetes.io/name: {{ include "kubeip.name" . }} 19 | spec: 20 | serviceAccountName: {{ include "kubeip.serviceAccountName" . | quote }} 21 | terminationGracePeriodSeconds: {{ .Values.daemonSet.terminationGracePeriodSeconds }} 22 | priorityClassName: {{ .Values.daemonSet.priorityClassName | quote }} 23 | nodeSelector: 24 | {{- if .Values.daemonSet.nodeSelector }} 25 | {{- toYaml .Values.daemonSet.nodeSelector | nindent 8 }} 26 | {{- end }} 27 | tolerations: 28 | - operator: "Exists" 29 | effect: "NoSchedule" 30 | - operator: "Exists" 31 | effect: "NoExecute" 32 | securityContext: 33 | runAsNonRoot: true 34 | runAsUser: 1001 35 | runAsGroup: 1001 36 | fsGroup: 1001 37 | containers: 38 | - name: kubeip 39 | image: "{{ .Values.image.repository }}" 40 | imagePullPolicy: Always 41 | resources: 42 | {{- toYaml .Values.daemonSet.resources | nindent 12 }} 43 | {{- if eq .Values.cloudProvider "oci" }} 44 | volumeMounts: 45 | - name: oci-config 46 | mountPath: /root/.oci 47 | {{- end }} 48 | env: 49 | - name: NODE_NAME 50 | valueFrom: 51 | fieldRef: 52 | fieldPath: spec.nodeName 53 | - name: FILTER 54 | value: {{ .Values.daemonSet.env.FILTER | quote }} 55 | - name: TAINT_KEY 56 | value: {{ .Values.daemonSet.env.TAINT_KEY | quote }} 57 | - name: LOG_LEVEL 58 | value: {{ .Values.daemonSet.env.LOG_LEVEL | quote }} 59 | - name: LOG_JSON 60 | value: {{ .Values.daemonSet.env.LOG_JSON | quote }} 61 | {{- if eq .Values.cloudProvider "oci" }} 62 | - name: OCI_CONFIG_FILE 63 | value: /root/.oci/config 64 | {{- end }} 65 | securityContext: 66 | privileged: false 67 | allowPrivilegeEscalation: false 68 | capabilities: 69 | drop: 70 | - ALL 71 | readOnlyRootFilesystem: true 72 | {{- if eq .Values.cloudProvider "oci" }} 73 | volumes: 74 | - name: oci-config 75 | secret: 76 | secretName: oci-config 77 | {{- end }} 78 | -------------------------------------------------------------------------------- /internal/types/filters_test.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | func Test_types_CheckFreeformTagFilter(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | filter OCIFilters 13 | target map[string]string 14 | want bool 15 | }{ 16 | { 17 | name: "nil filter", 18 | filter: OCIFilters{FreeformTags: nil}, 19 | target: map[string]string{"key1": "value1"}, 20 | want: true, 21 | }, 22 | { 23 | name: "nil target", 24 | filter: OCIFilters{FreeformTags: map[string]string{"key1": "value1"}}, 25 | target: nil, 26 | want: false, 27 | }, 28 | { 29 | name: "matching filter", 30 | filter: OCIFilters{FreeformTags: map[string]string{"key1": "value1"}}, 31 | target: map[string]string{"key1": "value1"}, 32 | want: true, 33 | }, 34 | { 35 | name: "non-matching filter", 36 | filter: OCIFilters{FreeformTags: map[string]string{"key1": "value1"}}, 37 | target: map[string]string{"key1": "value2"}, 38 | want: false, 39 | }, 40 | { 41 | name: "partial match", 42 | filter: OCIFilters{FreeformTags: map[string]string{"key1": "value1", "key2": "value2"}}, 43 | target: map[string]string{"key1": "value1"}, 44 | want: false, 45 | }, 46 | } 47 | 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | if got := tt.filter.CheckFreeformTagFilter(tt.target); got != tt.want { 51 | t.Errorf("CheckFreeformTagFilter() = %v, want %v", got, tt.want) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func Test_types_ParseFreeformTagFilter(t *testing.T) { 58 | tests := []struct { 59 | name string 60 | filter string 61 | wantKey string 62 | wantVal string 63 | wantErr error 64 | }{ 65 | { 66 | name: "valid filter", 67 | filter: "freeformTags.key=value", 68 | wantKey: "key", 69 | wantVal: "value", 70 | wantErr: nil, 71 | }, 72 | { 73 | name: "invalid filter format", 74 | filter: "freeformTags.keyvalue", 75 | wantKey: "", 76 | wantVal: "", 77 | wantErr: errors.New("invalid filter format for freeform tags, should be in format freeformTags.key=value, found: freeformTags.keyvalue"), 78 | }, 79 | { 80 | name: "missing prefix", 81 | filter: "key=value", 82 | wantKey: "", 83 | wantVal: "", 84 | wantErr: errors.New("invalid filter format for freeform tags, should be in format freeformTags.key=value, found: key=value"), 85 | }, 86 | } 87 | 88 | for _, tt := range tests { 89 | t.Run(tt.name, func(t *testing.T) { 90 | gotKey, gotVal, err := ParseFreeformTagFilter(tt.filter) 91 | if gotKey != tt.wantKey || gotVal != tt.wantVal || (err != nil && err.Error() != tt.wantErr.Error()) { 92 | t.Errorf("ParseFreeformTagFilter() = (%v, %v, %v), want (%v, %v, %v)", gotKey, gotVal, err, tt.wantKey, tt.wantVal, tt.wantErr) 93 | } 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /mocks/node/Explorer.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | 10 | types "github.com/doitintl/kubeip/internal/types" 11 | ) 12 | 13 | // Explorer is an autogenerated mock type for the Explorer type 14 | type Explorer struct { 15 | mock.Mock 16 | } 17 | 18 | type Explorer_Expecter struct { 19 | mock *mock.Mock 20 | } 21 | 22 | func (_m *Explorer) EXPECT() *Explorer_Expecter { 23 | return &Explorer_Expecter{mock: &_m.Mock} 24 | } 25 | 26 | // GetNode provides a mock function with given fields: ctx, nodeName 27 | func (_m *Explorer) GetNode(ctx context.Context, nodeName string) (*types.Node, error) { 28 | ret := _m.Called(ctx, nodeName) 29 | 30 | var r0 *types.Node 31 | var r1 error 32 | if rf, ok := ret.Get(0).(func(context.Context, string) (*types.Node, error)); ok { 33 | return rf(ctx, nodeName) 34 | } 35 | if rf, ok := ret.Get(0).(func(context.Context, string) *types.Node); ok { 36 | r0 = rf(ctx, nodeName) 37 | } else { 38 | if ret.Get(0) != nil { 39 | r0 = ret.Get(0).(*types.Node) 40 | } 41 | } 42 | 43 | if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { 44 | r1 = rf(ctx, nodeName) 45 | } else { 46 | r1 = ret.Error(1) 47 | } 48 | 49 | return r0, r1 50 | } 51 | 52 | // Explorer_GetNode_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetNode' 53 | type Explorer_GetNode_Call struct { 54 | *mock.Call 55 | } 56 | 57 | // GetNode is a helper method to define mock.On call 58 | // - ctx context.Context 59 | // - nodeName string 60 | func (_e *Explorer_Expecter) GetNode(ctx interface{}, nodeName interface{}) *Explorer_GetNode_Call { 61 | return &Explorer_GetNode_Call{Call: _e.mock.On("GetNode", ctx, nodeName)} 62 | } 63 | 64 | func (_c *Explorer_GetNode_Call) Run(run func(ctx context.Context, nodeName string)) *Explorer_GetNode_Call { 65 | _c.Call.Run(func(args mock.Arguments) { 66 | run(args[0].(context.Context), args[1].(string)) 67 | }) 68 | return _c 69 | } 70 | 71 | func (_c *Explorer_GetNode_Call) Return(_a0 *types.Node, _a1 error) *Explorer_GetNode_Call { 72 | _c.Call.Return(_a0, _a1) 73 | return _c 74 | } 75 | 76 | func (_c *Explorer_GetNode_Call) RunAndReturn(run func(context.Context, string) (*types.Node, error)) *Explorer_GetNode_Call { 77 | _c.Call.Return(run) 78 | return _c 79 | } 80 | 81 | // NewExplorer creates a new instance of Explorer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 82 | // The first argument is typically a *testing.T value. 83 | func NewExplorer(t interface { 84 | mock.TestingT 85 | Cleanup(func()) 86 | }) *Explorer { 87 | mock := &Explorer{} 88 | mock.Mock.Test(t) 89 | 90 | t.Cleanup(func() { mock.AssertExpectations(t) }) 91 | 92 | return mock 93 | } 94 | -------------------------------------------------------------------------------- /mocks/cloud/InstanceGetter.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | mock "github.com/stretchr/testify/mock" 7 | compute "google.golang.org/api/compute/v1" 8 | ) 9 | 10 | // InstanceGetter is an autogenerated mock type for the InstanceGetter type 11 | type InstanceGetter struct { 12 | mock.Mock 13 | } 14 | 15 | type InstanceGetter_Expecter struct { 16 | mock *mock.Mock 17 | } 18 | 19 | func (_m *InstanceGetter) EXPECT() *InstanceGetter_Expecter { 20 | return &InstanceGetter_Expecter{mock: &_m.Mock} 21 | } 22 | 23 | // Get provides a mock function with given fields: projectID, zone, instance 24 | func (_m *InstanceGetter) Get(projectID string, zone string, instance string) (*compute.Instance, error) { 25 | ret := _m.Called(projectID, zone, instance) 26 | 27 | var r0 *compute.Instance 28 | var r1 error 29 | if rf, ok := ret.Get(0).(func(string, string, string) (*compute.Instance, error)); ok { 30 | return rf(projectID, zone, instance) 31 | } 32 | if rf, ok := ret.Get(0).(func(string, string, string) *compute.Instance); ok { 33 | r0 = rf(projectID, zone, instance) 34 | } else { 35 | if ret.Get(0) != nil { 36 | r0 = ret.Get(0).(*compute.Instance) 37 | } 38 | } 39 | 40 | if rf, ok := ret.Get(1).(func(string, string, string) error); ok { 41 | r1 = rf(projectID, zone, instance) 42 | } else { 43 | r1 = ret.Error(1) 44 | } 45 | 46 | return r0, r1 47 | } 48 | 49 | // InstanceGetter_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' 50 | type InstanceGetter_Get_Call struct { 51 | *mock.Call 52 | } 53 | 54 | // Get is a helper method to define mock.On call 55 | // - projectID string 56 | // - zone string 57 | // - instance string 58 | func (_e *InstanceGetter_Expecter) Get(projectID interface{}, zone interface{}, instance interface{}) *InstanceGetter_Get_Call { 59 | return &InstanceGetter_Get_Call{Call: _e.mock.On("Get", projectID, zone, instance)} 60 | } 61 | 62 | func (_c *InstanceGetter_Get_Call) Run(run func(projectID string, zone string, instance string)) *InstanceGetter_Get_Call { 63 | _c.Call.Run(func(args mock.Arguments) { 64 | run(args[0].(string), args[1].(string), args[2].(string)) 65 | }) 66 | return _c 67 | } 68 | 69 | func (_c *InstanceGetter_Get_Call) Return(_a0 *compute.Instance, _a1 error) *InstanceGetter_Get_Call { 70 | _c.Call.Return(_a0, _a1) 71 | return _c 72 | } 73 | 74 | func (_c *InstanceGetter_Get_Call) RunAndReturn(run func(string, string, string) (*compute.Instance, error)) *InstanceGetter_Get_Call { 75 | _c.Call.Return(run) 76 | return _c 77 | } 78 | 79 | // NewInstanceGetter creates a new instance of InstanceGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 80 | // The first argument is typically a *testing.T value. 81 | func NewInstanceGetter(t interface { 82 | mock.TestingT 83 | Cleanup(func()) 84 | }) *InstanceGetter { 85 | mock := &InstanceGetter{} 86 | mock.Mock.Test(t) 87 | 88 | t.Cleanup(func() { mock.AssertExpectations(t) }) 89 | 90 | return mock 91 | } 92 | -------------------------------------------------------------------------------- /mocks/cloud/EipLister.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | types "github.com/aws/aws-sdk-go-v2/service/ec2/types" 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // EipLister is an autogenerated mock type for the EipLister type 13 | type EipLister struct { 14 | mock.Mock 15 | } 16 | 17 | type EipLister_Expecter struct { 18 | mock *mock.Mock 19 | } 20 | 21 | func (_m *EipLister) EXPECT() *EipLister_Expecter { 22 | return &EipLister_Expecter{mock: &_m.Mock} 23 | } 24 | 25 | // List provides a mock function with given fields: ctx, filter, inUse 26 | func (_m *EipLister) List(ctx context.Context, filter map[string][]string, inUse bool) ([]types.Address, error) { 27 | ret := _m.Called(ctx, filter, inUse) 28 | 29 | var r0 []types.Address 30 | var r1 error 31 | if rf, ok := ret.Get(0).(func(context.Context, map[string][]string, bool) ([]types.Address, error)); ok { 32 | return rf(ctx, filter, inUse) 33 | } 34 | if rf, ok := ret.Get(0).(func(context.Context, map[string][]string, bool) []types.Address); ok { 35 | r0 = rf(ctx, filter, inUse) 36 | } else { 37 | if ret.Get(0) != nil { 38 | r0 = ret.Get(0).([]types.Address) 39 | } 40 | } 41 | 42 | if rf, ok := ret.Get(1).(func(context.Context, map[string][]string, bool) error); ok { 43 | r1 = rf(ctx, filter, inUse) 44 | } else { 45 | r1 = ret.Error(1) 46 | } 47 | 48 | return r0, r1 49 | } 50 | 51 | // EipLister_List_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'List' 52 | type EipLister_List_Call struct { 53 | *mock.Call 54 | } 55 | 56 | // List is a helper method to define mock.On call 57 | // - ctx context.Context 58 | // - filter map[string][]string 59 | // - inUse bool 60 | func (_e *EipLister_Expecter) List(ctx interface{}, filter interface{}, inUse interface{}) *EipLister_List_Call { 61 | return &EipLister_List_Call{Call: _e.mock.On("List", ctx, filter, inUse)} 62 | } 63 | 64 | func (_c *EipLister_List_Call) Run(run func(ctx context.Context, filter map[string][]string, inUse bool)) *EipLister_List_Call { 65 | _c.Call.Run(func(args mock.Arguments) { 66 | run(args[0].(context.Context), args[1].(map[string][]string), args[2].(bool)) 67 | }) 68 | return _c 69 | } 70 | 71 | func (_c *EipLister_List_Call) Return(_a0 []types.Address, _a1 error) *EipLister_List_Call { 72 | _c.Call.Return(_a0, _a1) 73 | return _c 74 | } 75 | 76 | func (_c *EipLister_List_Call) RunAndReturn(run func(context.Context, map[string][]string, bool) ([]types.Address, error)) *EipLister_List_Call { 77 | _c.Call.Return(run) 78 | return _c 79 | } 80 | 81 | // NewEipLister creates a new instance of EipLister. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 82 | // The first argument is typically a *testing.T value. 83 | func NewEipLister(t interface { 84 | mock.TestingT 85 | Cleanup(func()) 86 | }) *EipLister { 87 | mock := &EipLister{} 88 | mock.Mock.Test(t) 89 | 90 | t.Cleanup(func() { mock.AssertExpectations(t) }) 91 | 92 | return mock 93 | } 94 | -------------------------------------------------------------------------------- /mocks/cloud/Ec2InstanceGetter.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | types "github.com/aws/aws-sdk-go-v2/service/ec2/types" 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // Ec2InstanceGetter is an autogenerated mock type for the Ec2InstanceGetter type 13 | type Ec2InstanceGetter struct { 14 | mock.Mock 15 | } 16 | 17 | type Ec2InstanceGetter_Expecter struct { 18 | mock *mock.Mock 19 | } 20 | 21 | func (_m *Ec2InstanceGetter) EXPECT() *Ec2InstanceGetter_Expecter { 22 | return &Ec2InstanceGetter_Expecter{mock: &_m.Mock} 23 | } 24 | 25 | // Get provides a mock function with given fields: ctx, instanceID, region 26 | func (_m *Ec2InstanceGetter) Get(ctx context.Context, instanceID string, region string) (*types.Instance, error) { 27 | ret := _m.Called(ctx, instanceID, region) 28 | 29 | var r0 *types.Instance 30 | var r1 error 31 | if rf, ok := ret.Get(0).(func(context.Context, string, string) (*types.Instance, error)); ok { 32 | return rf(ctx, instanceID, region) 33 | } 34 | if rf, ok := ret.Get(0).(func(context.Context, string, string) *types.Instance); ok { 35 | r0 = rf(ctx, instanceID, region) 36 | } else { 37 | if ret.Get(0) != nil { 38 | r0 = ret.Get(0).(*types.Instance) 39 | } 40 | } 41 | 42 | if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { 43 | r1 = rf(ctx, instanceID, region) 44 | } else { 45 | r1 = ret.Error(1) 46 | } 47 | 48 | return r0, r1 49 | } 50 | 51 | // Ec2InstanceGetter_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' 52 | type Ec2InstanceGetter_Get_Call struct { 53 | *mock.Call 54 | } 55 | 56 | // Get is a helper method to define mock.On call 57 | // - ctx context.Context 58 | // - instanceID string 59 | // - region string 60 | func (_e *Ec2InstanceGetter_Expecter) Get(ctx interface{}, instanceID interface{}, region interface{}) *Ec2InstanceGetter_Get_Call { 61 | return &Ec2InstanceGetter_Get_Call{Call: _e.mock.On("Get", ctx, instanceID, region)} 62 | } 63 | 64 | func (_c *Ec2InstanceGetter_Get_Call) Run(run func(ctx context.Context, instanceID string, region string)) *Ec2InstanceGetter_Get_Call { 65 | _c.Call.Run(func(args mock.Arguments) { 66 | run(args[0].(context.Context), args[1].(string), args[2].(string)) 67 | }) 68 | return _c 69 | } 70 | 71 | func (_c *Ec2InstanceGetter_Get_Call) Return(_a0 *types.Instance, _a1 error) *Ec2InstanceGetter_Get_Call { 72 | _c.Call.Return(_a0, _a1) 73 | return _c 74 | } 75 | 76 | func (_c *Ec2InstanceGetter_Get_Call) RunAndReturn(run func(context.Context, string, string) (*types.Instance, error)) *Ec2InstanceGetter_Get_Call { 77 | _c.Call.Return(run) 78 | return _c 79 | } 80 | 81 | // NewEc2InstanceGetter creates a new instance of Ec2InstanceGetter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 82 | // The first argument is typically a *testing.T value. 83 | func NewEc2InstanceGetter(t interface { 84 | mock.TestingT 85 | Cleanup(func()) 86 | }) *Ec2InstanceGetter { 87 | mock := &Ec2InstanceGetter{} 88 | mock.Mock.Test(t) 89 | 90 | t.Cleanup(func() { mock.AssertExpectations(t) }) 91 | 92 | return mock 93 | } 94 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # Go parameters 2 | GOCMD=go 3 | GOBUILD=$(GOCMD) build 4 | GORUN=$(GOCMD) run 5 | GOCLEAN=$(GOCMD) clean 6 | GOTEST=$(GOCMD) test 7 | GOGET=$(GOCMD) get 8 | GOTOOL=$(GOCMD) tool 9 | GOLINT=golangci-lint 10 | GOMOCK=mockery 11 | LINT_CONFIG = $(CURDIR)/.golangci.yaml 12 | 13 | BIN=$(CURDIR)/.bin 14 | BINARY_NAME=kubeip-agent 15 | TARGETOS := $(or $(TARGETOS), linux) 16 | TARGETARCH := $(or $(TARGETARCH), amd64) 17 | 18 | DATE ?= $(shell date +%FT%T%z) 19 | 20 | # get version from environment variable if set or use git describe (match SemVer) 21 | VERSION := $(if $(VERSION),$(VERSION),$(shell git describe --tags --always --dirty --match="[0-9]*.[0-9]*.[0-9]*" 2> /dev/null || \ 22 | cat $(CURDIR)/.version 2> /dev/null || echo v0)) 23 | 24 | # get commit from environment variable if set or use git commit 25 | COMMIT := $(if $(COMMIT),$(COMMIT),$(shell git rev-parse --short HEAD 2>/dev/null)) 26 | # get branch from environment variable if set or use git branch 27 | BRANCH := $(if $(BRANCH),$(BRANCH),$(shell git rev-parse --abbrev-ref HEAD 2>/dev/null)) 28 | 29 | Q = $(if $(filter 1,$V),,@) 30 | M = $(shell printf "\033[34;1m▶\033[0m") 31 | 32 | export CGO_ENABLED=0 33 | export GOOS=$(TARGETOS) 34 | export GOARCH=$(TARGETARCH) 35 | 36 | # main task 37 | all: lint test build ; $(info $(M) build, test and deploy ...) @ ## release cycle 38 | 39 | # Tools 40 | setup-lint: 41 | $(GOCMD) install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.1 42 | setup-mockery: 43 | $(GOCMD) install github.com/vektra/mockery/v2@v2.35.2 44 | 45 | # Tasks 46 | 47 | build: ; $(info $(M) building $(GOOS)/$(GOARCH) binary...) @ ## build with local Go SDK 48 | $(GOBUILD) -v \ 49 | -tags release \ 50 | -ldflags '-s -w -X main.version=$(VERSION) -X main.buildDate=$(DATE) -X main.gitCommit=$(COMMIT) -X main.gitBranch=$(BRANCH)' \ 51 | -o $(BIN)/$(BINARY_NAME) ./cmd/. 52 | 53 | lint: setup-lint; $(info $(M) running golangci-lint ...) @ ## run golangci-lint linters 54 | # updating path since golangci-lint is looking for go binary and this may lead to 55 | # conflict when multiple go versions are installed 56 | $Q $(GOLINT) run -v -c $(LINT_CONFIG) --out-format checkstyle ./... > golangci-lint.out 57 | 58 | mock: setup-mockery ; $(info $(M) running mockery ...) @ ## run mockery to generate mocks 59 | $Q $(GOMOCK) --dir internal --all --keeptree --with-expecter --exported 60 | 61 | test: ; $(info $(M) running test ...) @ ## run tests with coverage 62 | $Q $(GOCMD) fmt ./... 63 | $Q $(GOTEST) -v -cover ./... -coverprofile=coverage.out 64 | $Q $(GOTOOL) cover -func=coverage.out 65 | 66 | test-json: ; $(info $(M) running test output JSON ...) @ ## run tests with JSON report and coverage 67 | $Q $(GOTEST) -v -cover ./... -coverprofile=coverage.out -json > test-report.out 68 | 69 | precommit: lint test ; $(info $(M) test and lint ...) @ ## release cycle: test > lint 70 | 71 | testview: ; $(info $(M) generating coverage report ...) @ ## generate HTML coverage report 72 | $(GOTOOL) cover -html=coverage.out 73 | 74 | clean: ; $(info $(M) cleaning...) @ ## cleanup everything 75 | $Q $(GOCLEAN) 76 | @rm -rf $(BIN) 77 | @rm -rf test/tests.* test/coverage.* 78 | 79 | run: ; $(info $(M) running ...) @ ## run locally 80 | $Q $(GORUN) -v cmd/main.go 81 | 82 | help: ## display help 83 | @grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \ 84 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' 85 | -------------------------------------------------------------------------------- /examples/gcp/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/google" { 5 | version = "4.84.0" 6 | constraints = ">= 3.39.0, >= 3.53.0, < 5.0.0, < 6.0.0" 7 | hashes = [ 8 | "h1:fybaK74buTd4Ys2CUZm6jw7NXtSqtcLoW2jeNB4Ff2E=", 9 | "zh:0b3e945fa76876c312bdddca7b18c93b734998febb616b2ebb84a0a299ae97c2", 10 | "zh:1d47d00730fab764bddb6d548fed7e124739b0bcebb9f3b3c6aa247de55fb804", 11 | "zh:29bff92b4375a35a7729248b3bc5db8991ca1b9ba640fc25b13700e12f99c195", 12 | "zh:382353516e7d408a81f1a09a36f9015429be73ca3665367119aad88713209d9a", 13 | "zh:78afa20e25a690d076eeaafd7879993ef9763a8a1b6762e2cbe42330464cc1fa", 14 | "zh:8f6422e94de865669b33a2d9fb95a3e392e841988e890f7379a206e9d47e3415", 15 | "zh:be5c7b52c893b971c860146aec643f7007f34430106f101eab686ed81eccbd26", 16 | "zh:bfc37b641bf3378183eb3b8735554c3949a5cfaa8f76403d7eff38de1474b6d9", 17 | "zh:c834f88dc8eb21af992871ed13a221015ae3b051aeca7386662071026f1546b4", 18 | "zh:f3296c8c0d57dc28e23cf91717484264531655ac478d994584ebc73f70679471", 19 | "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", 20 | "zh:f8efe114ff4891776f48f7d2620b8d6963d3ddac6e42ce25bc761343da964c24", 21 | ] 22 | } 23 | 24 | provider "registry.terraform.io/hashicorp/google-beta" { 25 | version = "5.1.0" 26 | hashes = [ 27 | "h1:d0u9a3m826V3QZ7XsycvlJGduERSi1eunLoHWZjRIb0=", 28 | "zh:3369d685b81dfad4ee307c9729ea20f6593f1dee23bc785c167c6a0b6843f208", 29 | "zh:40cef4bbbcefc4944843915a626ab39b734ba0e7dcbe57ea9faed1e935b73efb", 30 | "zh:50dea4f57191c8a91dcbc9db3a09381899b43e06c3f2e6767792d8ce7711e8a1", 31 | "zh:5406877b75fdf94daeed74a69e65f6e02c40880cf22f5d91c75ca69c3c7f435a", 32 | "zh:7d9074317ca61384a86468d40e2f30f67eec5e44e87d2eac752cdaaed0a45e83", 33 | "zh:9188b5492c70f3826f65134f7c74ce74a933ced6e28426e9d6a9358d8c33b13d", 34 | "zh:b06dabf01ca9f9a0cf2c0613d00a212ae2b8c2b7d3e78057f52856e385483c87", 35 | "zh:b7ac631dbd6efea37ca94bae7d0476a13243d884a8bd8eb2d39e3398c1e9b9ad", 36 | "zh:c927efccfab1e3afb1fdc3ba141d0e04f67fffadb55346b2b4b272a1e358fe8a", 37 | "zh:d418d657c7b95762b6d5caae993ccc18bf54c63dec08c5a03f0aeb53403440f4", 38 | "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", 39 | "zh:f9e96ead5152fbb2df8571ee87810e7c3638df877ba5124e2b092faf4a3a641e", 40 | ] 41 | } 42 | 43 | provider "registry.terraform.io/hashicorp/kubernetes" { 44 | version = "2.23.0" 45 | hashes = [ 46 | "h1:arTzD0XG/DswGCAx9JEttkSKe9RyyFW9W7UWcXF13dU=", 47 | "zh:10488a12525ed674359585f83e3ee5e74818b5c98e033798351678b21b2f7d89", 48 | "zh:1102ba5ca1a595f880e67102bbf999cc8b60203272a078a5b1e896d173f3f34b", 49 | "zh:1347cf958ed3f3f80b3c7b3e23ddda3d6c6573a81847a8ee92b7df231c238bf6", 50 | "zh:2cb18e9f5156bc1b1ee6bc580a709f7c2737d142722948f4a6c3c8efe757fa8d", 51 | "zh:5506aa6f28dcca2a265ccf8e34478b5ec2cb43b867fe6d93b0158f01590fdadd", 52 | "zh:6217a20686b631b1dcb448ee4bc795747ebc61b56fbe97a1ad51f375ebb0d996", 53 | "zh:8accf916c00579c22806cb771e8909b349ffb7eb29d9c5468d0a3f3166c7a84a", 54 | "zh:9379b0b54a0fa030b19c7b9356708ec8489e194c3b5e978df2d31368563308e5", 55 | "zh:aa99c580890691036c2931841e88e7ee80d59ae52289c8c2c28ea0ac23e31520", 56 | "zh:c57376d169875990ac68664d227fb69cd0037b92d0eba6921d757c3fd1879080", 57 | "zh:e6068e3f94f6943b5586557b73f109debe19d1a75ca9273a681d22d1ce066579", 58 | "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /mocks/cloud/OCIInstanceService.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | core "github.com/oracle/oci-go-sdk/v65/core" 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // OCIInstanceService is an autogenerated mock type for the OCIInstanceService type 13 | type OCIInstanceService struct { 14 | mock.Mock 15 | } 16 | 17 | type OCIInstanceService_Expecter struct { 18 | mock *mock.Mock 19 | } 20 | 21 | func (_m *OCIInstanceService) EXPECT() *OCIInstanceService_Expecter { 22 | return &OCIInstanceService_Expecter{mock: &_m.Mock} 23 | } 24 | 25 | // ListVnicAttachments provides a mock function with given fields: ctx, compartmentOCID, instanceOCID 26 | func (_m *OCIInstanceService) ListVnicAttachments(ctx context.Context, compartmentOCID string, instanceOCID string) ([]core.VnicAttachment, error) { 27 | ret := _m.Called(ctx, compartmentOCID, instanceOCID) 28 | 29 | if len(ret) == 0 { 30 | panic("no return value specified for ListVnicAttachments") 31 | } 32 | 33 | var r0 []core.VnicAttachment 34 | var r1 error 35 | if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]core.VnicAttachment, error)); ok { 36 | return rf(ctx, compartmentOCID, instanceOCID) 37 | } 38 | if rf, ok := ret.Get(0).(func(context.Context, string, string) []core.VnicAttachment); ok { 39 | r0 = rf(ctx, compartmentOCID, instanceOCID) 40 | } else { 41 | if ret.Get(0) != nil { 42 | r0 = ret.Get(0).([]core.VnicAttachment) 43 | } 44 | } 45 | 46 | if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { 47 | r1 = rf(ctx, compartmentOCID, instanceOCID) 48 | } else { 49 | r1 = ret.Error(1) 50 | } 51 | 52 | return r0, r1 53 | } 54 | 55 | // OCIInstanceService_ListVnicAttachments_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListVnicAttachments' 56 | type OCIInstanceService_ListVnicAttachments_Call struct { 57 | *mock.Call 58 | } 59 | 60 | // ListVnicAttachments is a helper method to define mock.On call 61 | // - ctx context.Context 62 | // - compartmentOCID string 63 | // - instanceOCID string 64 | func (_e *OCIInstanceService_Expecter) ListVnicAttachments(ctx interface{}, compartmentOCID interface{}, instanceOCID interface{}) *OCIInstanceService_ListVnicAttachments_Call { 65 | return &OCIInstanceService_ListVnicAttachments_Call{Call: _e.mock.On("ListVnicAttachments", ctx, compartmentOCID, instanceOCID)} 66 | } 67 | 68 | func (_c *OCIInstanceService_ListVnicAttachments_Call) Run(run func(ctx context.Context, compartmentOCID string, instanceOCID string)) *OCIInstanceService_ListVnicAttachments_Call { 69 | _c.Call.Run(func(args mock.Arguments) { 70 | run(args[0].(context.Context), args[1].(string), args[2].(string)) 71 | }) 72 | return _c 73 | } 74 | 75 | func (_c *OCIInstanceService_ListVnicAttachments_Call) Return(_a0 []core.VnicAttachment, _a1 error) *OCIInstanceService_ListVnicAttachments_Call { 76 | _c.Call.Return(_a0, _a1) 77 | return _c 78 | } 79 | 80 | func (_c *OCIInstanceService_ListVnicAttachments_Call) RunAndReturn(run func(context.Context, string, string) ([]core.VnicAttachment, error)) *OCIInstanceService_ListVnicAttachments_Call { 81 | _c.Call.Return(run) 82 | return _c 83 | } 84 | 85 | // 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. 86 | // The first argument is typically a *testing.T value. 87 | func NewOCIInstanceService(t interface { 88 | mock.TestingT 89 | Cleanup(func()) 90 | }) *OCIInstanceService { 91 | mock := &OCIInstanceService{} 92 | mock.Mock.Test(t) 93 | 94 | t.Cleanup(func() { mock.AssertExpectations(t) }) 95 | 96 | return mock 97 | } 98 | -------------------------------------------------------------------------------- /mocks/cloud/WaitCall.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | cloud "github.com/doitintl/kubeip/internal/cloud" 7 | compute "google.golang.org/api/compute/v1" 8 | 9 | context "context" 10 | 11 | mock "github.com/stretchr/testify/mock" 12 | ) 13 | 14 | // WaitCall is an autogenerated mock type for the WaitCall type 15 | type WaitCall struct { 16 | mock.Mock 17 | } 18 | 19 | type WaitCall_Expecter struct { 20 | mock *mock.Mock 21 | } 22 | 23 | func (_m *WaitCall) EXPECT() *WaitCall_Expecter { 24 | return &WaitCall_Expecter{mock: &_m.Mock} 25 | } 26 | 27 | // Context provides a mock function with given fields: ctx 28 | func (_m *WaitCall) Context(ctx context.Context) cloud.WaitCall { 29 | ret := _m.Called(ctx) 30 | 31 | var r0 cloud.WaitCall 32 | if rf, ok := ret.Get(0).(func(context.Context) cloud.WaitCall); ok { 33 | r0 = rf(ctx) 34 | } else { 35 | if ret.Get(0) != nil { 36 | r0 = ret.Get(0).(cloud.WaitCall) 37 | } 38 | } 39 | 40 | return r0 41 | } 42 | 43 | // WaitCall_Context_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Context' 44 | type WaitCall_Context_Call struct { 45 | *mock.Call 46 | } 47 | 48 | // Context is a helper method to define mock.On call 49 | // - ctx context.Context 50 | func (_e *WaitCall_Expecter) Context(ctx interface{}) *WaitCall_Context_Call { 51 | return &WaitCall_Context_Call{Call: _e.mock.On("Context", ctx)} 52 | } 53 | 54 | func (_c *WaitCall_Context_Call) Run(run func(ctx context.Context)) *WaitCall_Context_Call { 55 | _c.Call.Run(func(args mock.Arguments) { 56 | run(args[0].(context.Context)) 57 | }) 58 | return _c 59 | } 60 | 61 | func (_c *WaitCall_Context_Call) Return(_a0 cloud.WaitCall) *WaitCall_Context_Call { 62 | _c.Call.Return(_a0) 63 | return _c 64 | } 65 | 66 | func (_c *WaitCall_Context_Call) RunAndReturn(run func(context.Context) cloud.WaitCall) *WaitCall_Context_Call { 67 | _c.Call.Return(run) 68 | return _c 69 | } 70 | 71 | // Do provides a mock function with given fields: 72 | func (_m *WaitCall) Do() (*compute.Operation, error) { 73 | ret := _m.Called() 74 | 75 | var r0 *compute.Operation 76 | var r1 error 77 | if rf, ok := ret.Get(0).(func() (*compute.Operation, error)); ok { 78 | return rf() 79 | } 80 | if rf, ok := ret.Get(0).(func() *compute.Operation); ok { 81 | r0 = rf() 82 | } else { 83 | if ret.Get(0) != nil { 84 | r0 = ret.Get(0).(*compute.Operation) 85 | } 86 | } 87 | 88 | if rf, ok := ret.Get(1).(func() error); ok { 89 | r1 = rf() 90 | } else { 91 | r1 = ret.Error(1) 92 | } 93 | 94 | return r0, r1 95 | } 96 | 97 | // WaitCall_Do_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Do' 98 | type WaitCall_Do_Call struct { 99 | *mock.Call 100 | } 101 | 102 | // Do is a helper method to define mock.On call 103 | func (_e *WaitCall_Expecter) Do() *WaitCall_Do_Call { 104 | return &WaitCall_Do_Call{Call: _e.mock.On("Do")} 105 | } 106 | 107 | func (_c *WaitCall_Do_Call) Run(run func()) *WaitCall_Do_Call { 108 | _c.Call.Run(func(args mock.Arguments) { 109 | run() 110 | }) 111 | return _c 112 | } 113 | 114 | func (_c *WaitCall_Do_Call) Return(_a0 *compute.Operation, _a1 error) *WaitCall_Do_Call { 115 | _c.Call.Return(_a0, _a1) 116 | return _c 117 | } 118 | 119 | func (_c *WaitCall_Do_Call) RunAndReturn(run func() (*compute.Operation, error)) *WaitCall_Do_Call { 120 | _c.Call.Return(run) 121 | return _c 122 | } 123 | 124 | // NewWaitCall creates a new instance of WaitCall. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 125 | // The first argument is typically a *testing.T value. 126 | func NewWaitCall(t interface { 127 | mock.TestingT 128 | Cleanup(func()) 129 | }) *WaitCall { 130 | mock := &WaitCall{} 131 | mock.Mock.Test(t) 132 | 133 | t.Cleanup(func() { mock.AssertExpectations(t) }) 134 | 135 | return mock 136 | } 137 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | # which dirs to skip 3 | skip-dirs: 4 | - mocks 5 | # Timeout for analysis, e.g. 30s, 5m. 6 | # Default: 1m 7 | timeout: 5m 8 | # Exit code when at least one issue was found. 9 | # Default: 1 10 | issues-exit-code: 2 11 | # Include test files or not. 12 | # Default: true 13 | tests: false 14 | # allow parallel run 15 | allow-parallel-runners: true 16 | 17 | linters-settings: 18 | govet: 19 | check-shadowing: true 20 | gocyclo: 21 | min-complexity: 15 22 | maligned: 23 | suggest-new: true 24 | dupl: 25 | threshold: 100 26 | goconst: 27 | min-len: 2 28 | min-occurrences: 2 29 | misspell: 30 | locale: US 31 | ignore-words: 32 | - "cancelled" 33 | goimports: 34 | local-prefixes: github.com/golangci/golangci-lint 35 | gosec: 36 | excludes: 37 | - G601 38 | gocritic: 39 | enabled-tags: 40 | - diagnostic 41 | - experimental 42 | - opinionated 43 | - performance 44 | - style 45 | disabled-checks: 46 | - dupImport # https://github.com/go-critic/go-critic/issues/845 47 | - ifElseChain 48 | - octalLiteral 49 | - rangeValCopy 50 | - unnamedResult 51 | - whyNoLint 52 | - wrapperFunc 53 | funlen: 54 | lines: 105 55 | statements: 50 56 | tagliatelle: 57 | case: 58 | use-field-name: true 59 | rules: 60 | json: snake 61 | 62 | linters: 63 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 64 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 65 | disable-all: true 66 | enable: 67 | - asasalint 68 | - asciicheck 69 | - bidichk 70 | - bodyclose 71 | # - containedctx 72 | - contextcheck 73 | - decorder 74 | # - depguard 75 | - dogsled 76 | - dupword 77 | - dupl 78 | - durationcheck 79 | - errcheck 80 | - errchkjson 81 | - errname 82 | - errorlint 83 | - execinquery 84 | - exhaustive 85 | # - exhaustivestruct TODO: check how to fix it 86 | - exportloopref 87 | # - forbidigo TODO: configure forbidden code patterns 88 | # - forcetypeassert 89 | - funlen 90 | - gci 91 | # - gochecknoglobals TODO: remove globals from code 92 | # - gochecknoinits TODO: remove main.init 93 | - gochecksumtype 94 | - gocognit 95 | - goconst 96 | - gocritic 97 | - gocyclo 98 | # - godot 99 | # - godox 100 | - goerr113 101 | - gofmt 102 | - goimports 103 | - gomnd 104 | # - gomoddirectives 105 | - gomodguard 106 | - goprintffuncname 107 | - gosec 108 | - gosimple 109 | - govet 110 | - gosmopolitan 111 | - grouper 112 | - importas 113 | # - ireturn TODO: not sure if it is a good linter 114 | - ineffassign 115 | - interfacebloat 116 | - loggercheck 117 | - maintidx 118 | - makezero 119 | - mirror 120 | - misspell 121 | - musttag 122 | - nakedret 123 | # - nestif 124 | - nilerr 125 | - nilnil 126 | # - noctx 127 | - nolintlint 128 | - nonamedreturns 129 | - nosprintfhostport 130 | - paralleltest 131 | - perfsprint 132 | - prealloc 133 | - predeclared 134 | - promlinter 135 | - protogetter 136 | - reassign 137 | - revive 138 | - sloglint 139 | - spancheck 140 | - sqlclosecheck 141 | # - staticcheck 142 | - stylecheck 143 | # - tagalign 144 | # - tagliatelle 145 | - tenv 146 | - testableexamples 147 | - typecheck 148 | - unconvert 149 | - unparam 150 | - unused 151 | - usestdlibvars 152 | # - varnamelen TODO: review naming 153 | - whitespace 154 | - wrapcheck 155 | # - wsl 156 | - zerologlint 157 | 158 | issues: 159 | exclude-rules: 160 | - path: _test\.go 161 | linters: 162 | - funlen 163 | - bodyclose 164 | - gosec 165 | - dupl 166 | - gocognit 167 | - goconst 168 | - gocyclo 169 | exclude: 170 | - Using the variable on range scope `tt` in function literal 171 | -------------------------------------------------------------------------------- /internal/lease/lock_test.go: -------------------------------------------------------------------------------- 1 | package lease 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | "k8s.io/api/coordination/v1" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/client-go/kubernetes/fake" 15 | "k8s.io/utils/ptr" 16 | ) 17 | 18 | func TestLockAndUnlock(t *testing.T) { 19 | tests := []struct { 20 | name string 21 | leaseExists bool 22 | holderIdentity string 23 | leaseCurrentHolder *string 24 | leaseDuration int 25 | skipLock bool 26 | expectLockErr bool 27 | expectUnlockErr bool 28 | }{ 29 | { 30 | name: "Lock acquires lease when none exists", 31 | holderIdentity: "test-holder", 32 | leaseExists: false, 33 | leaseDuration: 1, 34 | }, 35 | { 36 | name: "Lock acquires lease when held by another and expires", 37 | leaseExists: true, 38 | holderIdentity: "test-holder", 39 | leaseCurrentHolder: ptr.To("another-holder"), 40 | leaseDuration: 2, 41 | }, 42 | { 43 | name: "Unlock releases lease", 44 | leaseExists: true, 45 | holderIdentity: "test-holder", 46 | leaseCurrentHolder: ptr.To("test-holder"), 47 | leaseDuration: 1, 48 | }, 49 | { 50 | name: "Unlock does not release lease when locked by another holder", 51 | leaseExists: true, 52 | leaseCurrentHolder: ptr.To("another-holder"), 53 | holderIdentity: "test-holder", 54 | leaseDuration: 1, 55 | skipLock: true, 56 | }, 57 | } 58 | 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | client := fake.NewSimpleClientset() 62 | if tt.leaseExists { 63 | timestamp := metav1.MicroTime{Time: time.Now()} 64 | lease := &v1.Lease{ 65 | ObjectMeta: metav1.ObjectMeta{ 66 | Name: "test-lease", 67 | Namespace: "test-namespace", 68 | }, 69 | Spec: v1.LeaseSpec{ 70 | HolderIdentity: tt.leaseCurrentHolder, 71 | LeaseDurationSeconds: ptr.To(int32(1)), 72 | AcquireTime: ×tamp, 73 | RenewTime: ×tamp, 74 | }, 75 | } 76 | 77 | _, err := client.CoordinationV1().Leases("test-namespace").Create(context.Background(), lease, metav1.CreateOptions{}) 78 | require.NoError(t, err) 79 | } 80 | 81 | lock := NewKubeLeaseLock(client, "test-lease", "test-namespace", tt.holderIdentity, tt.leaseDuration) 82 | 83 | if !tt.skipLock { 84 | err := lock.Lock(context.Background()) 85 | if tt.expectLockErr { 86 | assert.Error(t, err) 87 | } else { 88 | assert.NoError(t, err) 89 | } 90 | } 91 | 92 | err := lock.Unlock(context.Background()) 93 | if tt.expectUnlockErr { 94 | assert.Error(t, err) 95 | } else { 96 | assert.NoError(t, err) 97 | } 98 | }) 99 | } 100 | } 101 | 102 | func TestConcurrentLock(t *testing.T) { 103 | client := fake.NewSimpleClientset() 104 | 105 | var wg sync.WaitGroup 106 | wg.Add(2) 107 | 108 | // Goroutine 1: Acquire the lock and hold it for 5 seconds 109 | go func() { 110 | defer wg.Done() 111 | lock := NewKubeLeaseLock(client, "test-lease", "test-namespace", "test-holder-1", 5) 112 | err := lock.Lock(context.Background()) 113 | assert.NoError(t, err) 114 | fmt.Println("Lock acquired by goroutine 1") 115 | time.Sleep(2 * time.Second) 116 | err = lock.Unlock(context.Background()) 117 | assert.NoError(t, err) 118 | fmt.Println("Lock released by goroutine 1") 119 | }() 120 | 121 | time.Sleep(100 * time.Millisecond) 122 | 123 | // Goroutine 2: Try to acquire the lock and wait until it succeeds 124 | go func() { 125 | defer wg.Done() 126 | lock := NewKubeLeaseLock(client, "test-lease", "test-namespace", "test-holder-2", 5) 127 | err := lock.Lock(context.Background()) 128 | assert.NoError(t, err) 129 | fmt.Println("Lock acquired by goroutine 2") 130 | err = lock.Unlock(context.Background()) 131 | assert.NoError(t, err) 132 | fmt.Println("Lock released by goroutine 2") 133 | }() 134 | 135 | wg.Wait() 136 | } 137 | -------------------------------------------------------------------------------- /mocks/cloud/EipAssigner.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // EipAssigner is an autogenerated mock type for the EipAssigner type 12 | type EipAssigner struct { 13 | mock.Mock 14 | } 15 | 16 | type EipAssigner_Expecter struct { 17 | mock *mock.Mock 18 | } 19 | 20 | func (_m *EipAssigner) EXPECT() *EipAssigner_Expecter { 21 | return &EipAssigner_Expecter{mock: &_m.Mock} 22 | } 23 | 24 | // Assign provides a mock function with given fields: ctx, networkInterfaceID, allocationID 25 | func (_m *EipAssigner) Assign(ctx context.Context, networkInterfaceID string, allocationID string) error { 26 | ret := _m.Called(ctx, networkInterfaceID, allocationID) 27 | 28 | var r0 error 29 | if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { 30 | r0 = rf(ctx, networkInterfaceID, allocationID) 31 | } else { 32 | r0 = ret.Error(0) 33 | } 34 | 35 | return r0 36 | } 37 | 38 | // EipAssigner_Assign_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Assign' 39 | type EipAssigner_Assign_Call struct { 40 | *mock.Call 41 | } 42 | 43 | // Assign is a helper method to define mock.On call 44 | // - ctx context.Context 45 | // - networkInterfaceID string 46 | // - allocationID string 47 | func (_e *EipAssigner_Expecter) Assign(ctx interface{}, networkInterfaceID interface{}, allocationID interface{}) *EipAssigner_Assign_Call { 48 | return &EipAssigner_Assign_Call{Call: _e.mock.On("Assign", ctx, networkInterfaceID, allocationID)} 49 | } 50 | 51 | func (_c *EipAssigner_Assign_Call) Run(run func(ctx context.Context, networkInterfaceID string, allocationID string)) *EipAssigner_Assign_Call { 52 | _c.Call.Run(func(args mock.Arguments) { 53 | run(args[0].(context.Context), args[1].(string), args[2].(string)) 54 | }) 55 | return _c 56 | } 57 | 58 | func (_c *EipAssigner_Assign_Call) Return(_a0 error) *EipAssigner_Assign_Call { 59 | _c.Call.Return(_a0) 60 | return _c 61 | } 62 | 63 | func (_c *EipAssigner_Assign_Call) RunAndReturn(run func(context.Context, string, string) error) *EipAssigner_Assign_Call { 64 | _c.Call.Return(run) 65 | return _c 66 | } 67 | 68 | // Unassign provides a mock function with given fields: ctx, associationID 69 | func (_m *EipAssigner) Unassign(ctx context.Context, associationID string) error { 70 | ret := _m.Called(ctx, associationID) 71 | 72 | var r0 error 73 | if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { 74 | r0 = rf(ctx, associationID) 75 | } else { 76 | r0 = ret.Error(0) 77 | } 78 | 79 | return r0 80 | } 81 | 82 | // EipAssigner_Unassign_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unassign' 83 | type EipAssigner_Unassign_Call struct { 84 | *mock.Call 85 | } 86 | 87 | // Unassign is a helper method to define mock.On call 88 | // - ctx context.Context 89 | // - associationID string 90 | func (_e *EipAssigner_Expecter) Unassign(ctx interface{}, associationID interface{}) *EipAssigner_Unassign_Call { 91 | return &EipAssigner_Unassign_Call{Call: _e.mock.On("Unassign", ctx, associationID)} 92 | } 93 | 94 | func (_c *EipAssigner_Unassign_Call) Run(run func(ctx context.Context, associationID string)) *EipAssigner_Unassign_Call { 95 | _c.Call.Run(func(args mock.Arguments) { 96 | run(args[0].(context.Context), args[1].(string)) 97 | }) 98 | return _c 99 | } 100 | 101 | func (_c *EipAssigner_Unassign_Call) Return(_a0 error) *EipAssigner_Unassign_Call { 102 | _c.Call.Return(_a0) 103 | return _c 104 | } 105 | 106 | func (_c *EipAssigner_Unassign_Call) RunAndReturn(run func(context.Context, string) error) *EipAssigner_Unassign_Call { 107 | _c.Call.Return(run) 108 | return _c 109 | } 110 | 111 | // NewEipAssigner creates a new instance of EipAssigner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 112 | // The first argument is typically a *testing.T value. 113 | func NewEipAssigner(t interface { 114 | mock.TestingT 115 | Cleanup(func()) 116 | }) *EipAssigner { 117 | mock := &EipAssigner{} 118 | mock.Mock.Test(t) 119 | 120 | t.Cleanup(func() { mock.AssertExpectations(t) }) 121 | 122 | return mock 123 | } 124 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - '*' 8 | tags: 9 | - '*' 10 | paths-ignore: 11 | - 'docs/**' 12 | - 'deploy/**' 13 | - '*.md' 14 | - '*.yaml' 15 | - '*.sh' 16 | pull_request: 17 | branches: 18 | - '*' 19 | 20 | jobs: 21 | 22 | validate: 23 | 24 | runs-on: ubuntu-latest 25 | if: ${{ !contains(github.event.head_commit.message,'[skip ci]') }} 26 | steps: 27 | - name: checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: setup go 31 | uses: actions/setup-go@v5 32 | with: 33 | go-version: 1.22 34 | 35 | - name: lint 36 | uses: golangci/golangci-lint-action@v4 37 | with: 38 | version: v1.57.1 39 | skip-pkg-cache: true 40 | args: --config .golangci.yaml --verbose ./... 41 | 42 | - name: test 43 | shell: sh 44 | env: 45 | CGO_ENABLED: 0 46 | run: | 47 | make test-json 48 | 49 | - name: upload test results 50 | uses: actions/upload-artifact@v3 51 | if: ${{ always() }} 52 | with: 53 | name: test-reports 54 | if-no-files-found: ignore 55 | path: | 56 | golangci-lint.out 57 | test-report.out 58 | coverage.out 59 | 60 | - name: SonarCloud scan 61 | uses: SonarSource/sonarcloud-github-action@master 62 | if: ${{ always() }} 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 65 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 66 | 67 | 68 | docker-build: 69 | 70 | runs-on: ubuntu-latest 71 | needs: validate 72 | # build only on master branch and tags 73 | if: ${{ 74 | !contains(github.event.head_commit.message, '[skip ci]') && 75 | ( 76 | (github.event_name == 'push' && (github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags/'))) || 77 | (github.event_name == 'pull_request' && github.event.pull_request.draft == false) 78 | ) 79 | }} 80 | steps: 81 | - name: checkout 82 | uses: actions/checkout@v4 83 | 84 | - name: get short sha 85 | id: short_sha 86 | run: echo ::set-output name=sha::$(git rev-parse --short HEAD) 87 | 88 | - name: get version 89 | id: version 90 | run: echo ::set-output name=version::$([[ -z "${{ github.event.pull_request.number }}" ]] && echo "sha-${{ steps.short_sha.outputs.sha }}" || echo "pr-${{ github.event.pull_request.number }}") 91 | 92 | - name: set up QEMU 93 | uses: docker/setup-qemu-action@v3 94 | 95 | - name: set up Docker buildx 96 | id: buildx 97 | uses: docker/setup-buildx-action@v3 98 | 99 | - name: login to DockerHub 100 | uses: docker/login-action@v3 101 | with: 102 | username: ${{ secrets.DOCKERHUB_USERNAME }} 103 | password: ${{ secrets.DOCKERHUB_TOKEN }} 104 | 105 | - name: prepare meta 106 | id: meta 107 | uses: docker/metadata-action@v5 108 | with: 109 | images: ${{ github.repository }}-agent 110 | tags: | 111 | type=ref,event=branch 112 | type=ref,event=pr 113 | type=semver,pattern={{version}} 114 | type=semver,pattern={{major}}.{{minor}} 115 | type=semver,pattern={{major}} 116 | type=sha 117 | labels: | 118 | github.run.id=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 119 | org.opencontainers.image.title=kubeip-agent 120 | org.opencontainers.image.description=kubeip agent 121 | org.opencontainers.image.vendor=DoiT International 122 | 123 | - name: build and push 124 | uses: docker/build-push-action@v5 125 | with: 126 | build-args: | 127 | VERSION=${{ steps.version.outputs.version }} 128 | COMMIT=${{ steps.short_sha.outputs.sha }} 129 | BRANCH=${{ github.ref_name }} 130 | push: true 131 | platforms: linux/amd64,linux/arm64 132 | tags: ${{ steps.meta.outputs.tags }} 133 | labels: ${{ steps.meta.outputs.labels }} 134 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/doitintl/kubeip 2 | 3 | go 1.21 4 | 5 | require ( 6 | cloud.google.com/go/compute/metadata v0.2.3 7 | github.com/aws/aws-sdk-go-v2 v1.26.0 8 | github.com/aws/aws-sdk-go-v2/config v1.27.9 9 | github.com/aws/aws-sdk-go-v2/service/ec2 v1.152.0 10 | github.com/oracle/oci-go-sdk/v65 v65.80.0 11 | github.com/pkg/errors v0.9.1 12 | github.com/sirupsen/logrus v1.9.3 13 | github.com/stretchr/testify v1.9.0 14 | github.com/urfave/cli/v2 v2.27.1 15 | google.golang.org/api v0.171.0 16 | k8s.io/api v0.29.3 17 | k8s.io/apimachinery v0.29.3 18 | k8s.io/client-go v0.29.3 19 | k8s.io/utils v0.0.0-20240310230437-4693a0247e57 20 | sigs.k8s.io/controller-runtime v0.17.2 21 | ) 22 | 23 | require ( 24 | cloud.google.com/go/compute v1.25.1 // indirect 25 | github.com/aws/aws-sdk-go-v2/credentials v1.17.9 // indirect 26 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.0 // indirect 27 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.4 // indirect 28 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.4 // indirect 29 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect 31 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.6 // indirect 32 | github.com/aws/aws-sdk-go-v2/service/sso v1.20.3 // indirect 33 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.3 // indirect 34 | github.com/aws/aws-sdk-go-v2/service/sts v1.28.5 // indirect 35 | github.com/aws/smithy-go v1.20.1 // indirect 36 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 37 | github.com/davecgh/go-spew v1.1.1 // indirect 38 | github.com/emicklei/go-restful/v3 v3.12.0 // indirect 39 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 40 | github.com/felixge/httpsnoop v1.0.4 // indirect 41 | github.com/go-logr/logr v1.4.1 // indirect 42 | github.com/go-logr/stdr v1.2.2 // indirect 43 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 44 | github.com/go-openapi/jsonreference v0.21.0 // indirect 45 | github.com/go-openapi/swag v0.23.0 // indirect 46 | github.com/gofrs/flock v0.8.1 // indirect 47 | github.com/gogo/protobuf v1.3.2 // indirect 48 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 49 | github.com/golang/protobuf v1.5.4 // indirect 50 | github.com/google/gnostic-models v0.6.8 // indirect 51 | github.com/google/gofuzz v1.2.0 // indirect 52 | github.com/google/s2a-go v0.1.7 // indirect 53 | github.com/google/uuid v1.6.0 // indirect 54 | github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect 55 | github.com/googleapis/gax-go/v2 v2.12.3 // indirect 56 | github.com/imdario/mergo v0.3.16 // indirect 57 | github.com/jmespath/go-jmespath v0.4.0 // indirect 58 | github.com/josharian/intern v1.0.0 // indirect 59 | github.com/json-iterator/go v1.1.12 // indirect 60 | github.com/mailru/easyjson v0.7.7 // indirect 61 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 62 | github.com/modern-go/reflect2 v1.0.2 // indirect 63 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 64 | github.com/pmezard/go-difflib v1.0.0 // indirect 65 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 66 | github.com/sony/gobreaker v0.5.0 // indirect 67 | github.com/spf13/pflag v1.0.5 // indirect 68 | github.com/stretchr/objx v0.5.2 // indirect 69 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect 70 | go.opencensus.io v0.24.0 // indirect 71 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect 72 | go.opentelemetry.io/otel v1.24.0 // indirect 73 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 74 | go.opentelemetry.io/otel/trace v1.24.0 // indirect 75 | golang.org/x/crypto v0.21.0 // indirect 76 | golang.org/x/net v0.22.0 // indirect 77 | golang.org/x/oauth2 v0.18.0 // indirect 78 | golang.org/x/sys v0.18.0 // indirect 79 | golang.org/x/term v0.18.0 // indirect 80 | golang.org/x/text v0.14.0 // indirect 81 | golang.org/x/time v0.5.0 // indirect 82 | golang.org/x/tools v0.19.0 // indirect 83 | google.golang.org/appengine v1.6.8 // indirect 84 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect 85 | google.golang.org/grpc v1.62.1 // indirect 86 | google.golang.org/protobuf v1.33.0 // indirect 87 | gopkg.in/inf.v0 v0.9.1 // indirect 88 | gopkg.in/yaml.v2 v2.4.0 // indirect 89 | gopkg.in/yaml.v3 v3.0.1 // indirect 90 | k8s.io/klog/v2 v2.120.1 // indirect 91 | k8s.io/kube-openapi v0.0.0-20240322212309-b815d8309940 // indirect 92 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 93 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 94 | sigs.k8s.io/yaml v1.4.0 // indirect 95 | ) 96 | -------------------------------------------------------------------------------- /mocks/address/Assigner.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.35.2. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | mock "github.com/stretchr/testify/mock" 9 | ) 10 | 11 | // Assigner is an autogenerated mock type for the Assigner type 12 | type Assigner struct { 13 | mock.Mock 14 | } 15 | 16 | type Assigner_Expecter struct { 17 | mock *mock.Mock 18 | } 19 | 20 | func (_m *Assigner) EXPECT() *Assigner_Expecter { 21 | return &Assigner_Expecter{mock: &_m.Mock} 22 | } 23 | 24 | // Assign provides a mock function with given fields: ctx, instanceID, zone, filter, orderBy 25 | func (_m *Assigner) Assign(ctx context.Context, instanceID string, zone string, filter []string, orderBy string) (string, error) { 26 | ret := _m.Called(ctx, instanceID, zone, filter, orderBy) 27 | 28 | var r0 string 29 | var r1 error 30 | if rf, ok := ret.Get(0).(func(context.Context, string, string, []string, string) (string, error)); ok { 31 | return rf(ctx, instanceID, zone, filter, orderBy) 32 | } 33 | if rf, ok := ret.Get(0).(func(context.Context, string, string, []string, string) string); ok { 34 | r0 = rf(ctx, instanceID, zone, filter, orderBy) 35 | } else { 36 | r0 = ret.Get(0).(string) 37 | } 38 | 39 | if rf, ok := ret.Get(1).(func(context.Context, string, string, []string, string) error); ok { 40 | r1 = rf(ctx, instanceID, zone, filter, orderBy) 41 | } else { 42 | r1 = ret.Error(1) 43 | } 44 | 45 | return r0, r1 46 | } 47 | 48 | // Assigner_Assign_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Assign' 49 | type Assigner_Assign_Call struct { 50 | *mock.Call 51 | } 52 | 53 | // Assign is a helper method to define mock.On call 54 | // - ctx context.Context 55 | // - instanceID string 56 | // - zone string 57 | // - filter []string 58 | // - orderBy string 59 | func (_e *Assigner_Expecter) Assign(ctx interface{}, instanceID interface{}, zone interface{}, filter interface{}, orderBy interface{}) *Assigner_Assign_Call { 60 | return &Assigner_Assign_Call{Call: _e.mock.On("Assign", ctx, instanceID, zone, filter, orderBy)} 61 | } 62 | 63 | func (_c *Assigner_Assign_Call) Run(run func(ctx context.Context, instanceID string, zone string, filter []string, orderBy string)) *Assigner_Assign_Call { 64 | _c.Call.Run(func(args mock.Arguments) { 65 | run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].([]string), args[4].(string)) 66 | }) 67 | return _c 68 | } 69 | 70 | func (_c *Assigner_Assign_Call) Return(_a0 string, _a1 error) *Assigner_Assign_Call { 71 | _c.Call.Return(_a0, _a1) 72 | return _c 73 | } 74 | 75 | func (_c *Assigner_Assign_Call) RunAndReturn(run func(context.Context, string, string, []string, string) (string, error)) *Assigner_Assign_Call { 76 | _c.Call.Return(run) 77 | return _c 78 | } 79 | 80 | // Unassign provides a mock function with given fields: ctx, instanceID, zone 81 | func (_m *Assigner) Unassign(ctx context.Context, instanceID string, zone string) error { 82 | ret := _m.Called(ctx, instanceID, zone) 83 | 84 | var r0 error 85 | if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { 86 | r0 = rf(ctx, instanceID, zone) 87 | } else { 88 | r0 = ret.Error(0) 89 | } 90 | 91 | return r0 92 | } 93 | 94 | // Assigner_Unassign_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unassign' 95 | type Assigner_Unassign_Call struct { 96 | *mock.Call 97 | } 98 | 99 | // Unassign is a helper method to define mock.On call 100 | // - ctx context.Context 101 | // - instanceID string 102 | // - zone string 103 | func (_e *Assigner_Expecter) Unassign(ctx interface{}, instanceID interface{}, zone interface{}) *Assigner_Unassign_Call { 104 | return &Assigner_Unassign_Call{Call: _e.mock.On("Unassign", ctx, instanceID, zone)} 105 | } 106 | 107 | func (_c *Assigner_Unassign_Call) Run(run func(ctx context.Context, instanceID string, zone string)) *Assigner_Unassign_Call { 108 | _c.Call.Run(func(args mock.Arguments) { 109 | run(args[0].(context.Context), args[1].(string), args[2].(string)) 110 | }) 111 | return _c 112 | } 113 | 114 | func (_c *Assigner_Unassign_Call) Return(_a0 error) *Assigner_Unassign_Call { 115 | _c.Call.Return(_a0) 116 | return _c 117 | } 118 | 119 | func (_c *Assigner_Unassign_Call) RunAndReturn(run func(context.Context, string, string) error) *Assigner_Unassign_Call { 120 | _c.Call.Return(run) 121 | return _c 122 | } 123 | 124 | // NewAssigner creates a new instance of Assigner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 125 | // The first argument is typically a *testing.T value. 126 | func NewAssigner(t interface { 127 | mock.TestingT 128 | Cleanup(func()) 129 | }) *Assigner { 130 | mock := &Assigner{} 131 | mock.Mock.Test(t) 132 | 133 | t.Cleanup(func() { mock.AssertExpectations(t) }) 134 | 135 | return mock 136 | } 137 | -------------------------------------------------------------------------------- /internal/lease/lock.go: -------------------------------------------------------------------------------- 1 | package lease 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | coordinationv1 "k8s.io/api/coordination/v1" 8 | "k8s.io/apimachinery/pkg/api/errors" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/util/wait" 11 | "k8s.io/client-go/kubernetes" 12 | "k8s.io/utils/ptr" 13 | ) 14 | 15 | type KubeLock interface { 16 | Lock(ctx context.Context) error 17 | Unlock(ctx context.Context) error 18 | } 19 | 20 | type kubeLeaseLock struct { 21 | client kubernetes.Interface 22 | leaseName string 23 | namespace string 24 | holderIdentity string 25 | leaseDuration int // seconds 26 | cancelFunc context.CancelFunc 27 | } 28 | 29 | func NewKubeLeaseLock(client kubernetes.Interface, leaseName, namespace, holderIdentity string, leaseDurationSeconds int) KubeLock { 30 | return &kubeLeaseLock{ 31 | client: client, 32 | leaseName: leaseName, 33 | namespace: namespace, 34 | holderIdentity: holderIdentity, 35 | leaseDuration: leaseDurationSeconds, 36 | } 37 | } 38 | 39 | func (k *kubeLeaseLock) Lock(ctx context.Context) error { 40 | backoff := wait.Backoff{ 41 | Duration: time.Second, // start with 1 second 42 | Factor: 1.5, //nolint:gomnd // multiply by 1.5 on each retry 43 | Jitter: 0.5, //nolint:gomnd // add 50% jitter to wait time on each retry 44 | Steps: 100, //nolint:gomnd // retry 100 times 45 | Cap: time.Hour, // but never wait more than 1 hour 46 | } 47 | 48 | return wait.ExponentialBackoff(backoff, func() (bool, error) { //nolint:wrapcheck 49 | timestamp := metav1.MicroTime{Time: time.Now()} 50 | lease := &coordinationv1.Lease{ 51 | ObjectMeta: metav1.ObjectMeta{ 52 | Name: k.leaseName, 53 | Namespace: k.namespace, 54 | }, 55 | Spec: coordinationv1.LeaseSpec{ 56 | HolderIdentity: &k.holderIdentity, 57 | LeaseDurationSeconds: ptr.To(int32(k.leaseDuration)), 58 | AcquireTime: ×tamp, 59 | RenewTime: ×tamp, 60 | }, 61 | } 62 | 63 | _, err := k.client.CoordinationV1().Leases(k.namespace).Create(ctx, lease, metav1.CreateOptions{}) 64 | if err != nil { 65 | if errors.IsAlreadyExists(err) { 66 | // If the lease already exists, check if it's held by another holder 67 | existingLease, getErr := k.client.CoordinationV1().Leases(k.namespace).Get(ctx, k.leaseName, metav1.GetOptions{}) 68 | if getErr != nil { 69 | return false, getErr //nolint:wrapcheck 70 | } 71 | // check if the lease is expired 72 | if existingLease.Spec.RenewTime != nil && time.Since(existingLease.Spec.RenewTime.Time) > time.Duration(k.leaseDuration)*time.Second { 73 | // If the lease is expired, delete it and retry 74 | delErr := k.client.CoordinationV1().Leases(k.namespace).Delete(ctx, k.leaseName, metav1.DeleteOptions{}) 75 | if delErr != nil { 76 | return false, delErr //nolint:wrapcheck 77 | } 78 | return false, nil 79 | } 80 | // check if the lease is held by another holder 81 | if existingLease.Spec.HolderIdentity != nil && *existingLease.Spec.HolderIdentity != k.holderIdentity { 82 | // If the lease is held by another holder, return false to retry 83 | return false, nil 84 | } 85 | return true, nil 86 | } 87 | return false, err //nolint:wrapcheck 88 | } 89 | 90 | // Create a child context with cancellation 91 | ctx, k.cancelFunc = context.WithCancel(ctx) 92 | go k.renewLeasePeriodically(ctx) 93 | 94 | return true, nil 95 | }) 96 | } 97 | 98 | func (k *kubeLeaseLock) renewLeasePeriodically(ctx context.Context) { 99 | // let's renew the lease every 1/2 of the lease duration; use milliseconds for ticker 100 | ticker := time.NewTicker(time.Duration(k.leaseDuration*500) * time.Millisecond) //nolint:gomnd 101 | defer ticker.Stop() 102 | 103 | for { 104 | select { 105 | case <-ticker.C: 106 | lease, err := k.client.CoordinationV1().Leases(k.namespace).Get(ctx, k.leaseName, metav1.GetOptions{}) 107 | if err != nil || lease.Spec.HolderIdentity == nil || *lease.Spec.HolderIdentity != k.holderIdentity { 108 | return 109 | } 110 | 111 | lease.Spec.RenewTime = &metav1.MicroTime{Time: time.Now()} 112 | k.client.CoordinationV1().Leases(k.namespace).Update(ctx, lease, metav1.UpdateOptions{}) //nolint:errcheck 113 | case <-ctx.Done(): 114 | // Exit the goroutine when the context is cancelled 115 | return 116 | } 117 | } 118 | } 119 | 120 | func (k *kubeLeaseLock) Unlock(ctx context.Context) error { 121 | // Call the cancel function to stop the lease renewal process 122 | if k.cancelFunc != nil { 123 | k.cancelFunc() 124 | } 125 | lease, err := k.client.CoordinationV1().Leases(k.namespace).Get(ctx, k.leaseName, metav1.GetOptions{}) 126 | if err != nil { 127 | return err //nolint:wrapcheck 128 | } 129 | 130 | if lease.Spec.HolderIdentity == nil || *lease.Spec.HolderIdentity != k.holderIdentity { 131 | return nil 132 | } 133 | 134 | return k.client.CoordinationV1().Leases(k.namespace).Delete(ctx, k.leaseName, metav1.DeleteOptions{}) //nolint:wrapcheck 135 | } 136 | -------------------------------------------------------------------------------- /internal/cloud/oci_network_service.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/doitintl/kubeip/internal/types" 7 | "github.com/oracle/oci-go-sdk/v65/common" 8 | "github.com/oracle/oci-go-sdk/v65/core" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // OCINetworkService is the interface for all network related operations in OCI (Virtual Network). 13 | type OCINetworkService interface { 14 | ListPublicIps(ctx context.Context, request *core.ListPublicIpsRequest, filters *types.OCIFilters) ([]core.PublicIp, error) 15 | GetPublicIP(ctx context.Context, publicIPOCID string) (*core.PublicIp, error) 16 | UpdatePublicIP(ctx context.Context, publicIPOCID, privateIPOCID string) error 17 | DeletePublicIP(ctx context.Context, publicIPOCID string) error 18 | GetPrimaryPrivateIPOfVnic(ctx context.Context, vnicOCID string) (*core.PrivateIp, error) 19 | GetPrimaryVnic(ctx context.Context, vnicAttachments []core.VnicAttachment) (*core.Vnic, error) 20 | } 21 | 22 | // ociNetworkService is the implementation of OCINetworkService. 23 | type ociNetworkService struct { 24 | client core.VirtualNetworkClient 25 | } 26 | 27 | // NewOCINetworkService creates a new instance of OCINetworkService. 28 | func NewOCINetworkService() (OCINetworkService, error) { 29 | client, err := core.NewVirtualNetworkClientWithConfigurationProvider(common.DefaultConfigProvider()) 30 | if err != nil { 31 | return nil, errors.Wrap(err, "failed to create OCI Virtual Network client") 32 | } 33 | 34 | return &ociNetworkService{client: client}, nil 35 | } 36 | 37 | // ListPublicIps lists all public IPs for the given request and applies the given filters. 38 | func (svc *ociNetworkService) ListPublicIps(ctx context.Context, request *core.ListPublicIpsRequest, filters *types.OCIFilters) ([]core.PublicIp, error) { 39 | response, err := svc.client.ListPublicIps(ctx, *request) 40 | if err != nil { 41 | return nil, errors.Wrap(err, "failed to list public IPs") 42 | } 43 | 44 | if response.Items == nil { 45 | return nil, errors.New("no public IPs found") 46 | } 47 | 48 | // Apply filters 49 | if filters != nil { 50 | list := []core.PublicIp{} 51 | for _, ip := range response.Items { 52 | if filters.CheckFreeformTagFilter(ip.FreeformTags) && filters.CheckDefinedTagFilter(ip.DefinedTags) { 53 | list = append(list, ip) 54 | } 55 | } 56 | return list, nil 57 | } 58 | 59 | return response.Items, nil 60 | } 61 | 62 | // GetPublicIP returns the public IP with the given OCID. 63 | func (svc *ociNetworkService) GetPublicIP(ctx context.Context, publicIPOCID string) (*core.PublicIp, error) { 64 | request := core.GetPublicIpRequest{ 65 | PublicIpId: common.String(publicIPOCID), 66 | } 67 | response, err := svc.client.GetPublicIp(ctx, request) 68 | if err != nil { 69 | return nil, errors.Wrap(err, "failed to get details of public IP OCID: %s"+publicIPOCID) 70 | } 71 | 72 | if response.PublicIp.Id == nil { 73 | return nil, errors.Errorf("no public IP found with OCID %s", publicIPOCID) 74 | } 75 | 76 | return &response.PublicIp, nil 77 | } 78 | 79 | // UpdatePublicIP updates the public IP with the given OCID. 80 | func (svc *ociNetworkService) UpdatePublicIP(ctx context.Context, publicIPOCID, privateIPOCID string) error { 81 | request := core.UpdatePublicIpRequest{ 82 | PublicIpId: common.String(publicIPOCID), 83 | UpdatePublicIpDetails: core.UpdatePublicIpDetails{ 84 | PrivateIpId: common.String(privateIPOCID), 85 | }, 86 | } 87 | if _, err := svc.client.UpdatePublicIp(ctx, request); err != nil { 88 | return errors.Wrap(err, "failed to update public IP") 89 | } 90 | 91 | return nil 92 | } 93 | 94 | // DeletePublicIP deletes the public IP with the given OCID. 95 | func (svc *ociNetworkService) DeletePublicIP(ctx context.Context, publicIPOCID string) error { 96 | request := core.DeletePublicIpRequest{ 97 | PublicIpId: common.String(publicIPOCID), 98 | } 99 | if _, err := svc.client.DeletePublicIp(ctx, request); err != nil { 100 | return errors.Wrap(err, "failed to delete public IP") 101 | } 102 | 103 | return nil 104 | } 105 | 106 | // GetPrimaryPrivateIPOfVnic returns the primary private IP of the given VNIC. 107 | func (svc *ociNetworkService) GetPrimaryPrivateIPOfVnic(ctx context.Context, vnicOCID string) (*core.PrivateIp, error) { 108 | request := core.ListPrivateIpsRequest{ 109 | VnicId: common.String(vnicOCID), 110 | } 111 | response, err := svc.client.ListPrivateIps(ctx, request) 112 | if err != nil { 113 | return nil, errors.Wrap(err, "failed to list private IPs for the VNIC %s"+vnicOCID) 114 | } 115 | 116 | if response.Items == nil { 117 | return nil, errors.New("no private IPs found for the VNIC %s" + vnicOCID) 118 | } 119 | 120 | // Loop through the private IPs and return the primary one 121 | for _, privateIP := range response.Items { 122 | if *privateIP.IsPrimary { 123 | return &privateIP, nil 124 | } 125 | } 126 | 127 | return nil, errors.New("no primary private IP found for the VNIC %s" + vnicOCID) 128 | } 129 | 130 | // GetPrimaryVnic returns the primary VNIC from the given VNIC attachments. 131 | func (svc *ociNetworkService) GetPrimaryVnic(ctx context.Context, vnicAttachments []core.VnicAttachment) (*core.Vnic, error) { 132 | for _, vnicAttachment := range vnicAttachments { 133 | vnic, err := svc.client.GetVnic(ctx, core.GetVnicRequest{VnicId: vnicAttachment.VnicId}) 134 | if err != nil { 135 | return nil, errors.Wrap(err, "failed to get VNIC details with OCID %s"+*vnicAttachment.VnicId) 136 | } 137 | 138 | if vnic.IsPrimary != nil && *vnic.IsPrimary { 139 | return &vnic.Vnic, nil 140 | } 141 | } 142 | 143 | return nil, errors.New("no primary VNIC found from the given VNIC attachments") 144 | } 145 | -------------------------------------------------------------------------------- /examples/aws/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "5.21.0" 6 | constraints = ">= 5.0.0" 7 | hashes = [ 8 | "h1:bCRZDV8QYPpl+zIU7karO77B4x2cvQ7UHKAGNvTFWQs=", 9 | "zh:1ba1411e4f8c047950db94c236f146d4590790320c68320b4e56082d8746a507", 10 | "zh:3185e4a34cfcad35dcf11439290a4bd0ad52d462eca2ab5d4940488a2db72833", 11 | "zh:3c6b901f874b4d9a85301a653d0bd507b052992bd84fc81100f4e5f73b1adab7", 12 | "zh:45d3fdbbc5804f295576b7155fdca527dedff17a014ed40c215af3bc60c329db", 13 | "zh:47b64b453d2c373062e47a54f3df33335dc29bce6ddbbf2da9e7be768c560abe", 14 | "zh:5cdf57ffd465288d9732d14ba13b377a8d389e0ba0ce3ac4773fd6fdfc09d6a1", 15 | "zh:81ec4c662581a2446c78da7b27d7e0d5c2e4d50925294789ec13661817f4b5a4", 16 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 17 | "zh:ac248464fd4ce1f020c05f27e3182532a7d1af4b8185a4b4be8b906b30b0ca5a", 18 | "zh:bbbedc6b6eaffcce0b31b397d607464f0c21c1b9406182163d504d3f392cc68d", 19 | "zh:c2afc111f9503829ed055e2ae91d873670c57bd16acc1a3246ac3957f6998d4e", 20 | "zh:cd3c8175b2152848113482da70e5b9c7cb4c951f2046fc0b832715300bd88b97", 21 | "zh:cf89b0c09d426d489f9477209d4084e64ad1b598036284fa688b41de626b58e6", 22 | "zh:d9d127637c3b9ff6e2d0a2c30f54bd48ab1de34f725a5df1a6a3d039b021e636", 23 | "zh:dccca1090e4054d6558218406385fb0421ab4ac3b75e121641973be481a81f01", 24 | ] 25 | } 26 | 27 | provider "registry.terraform.io/hashicorp/cloudinit" { 28 | version = "2.3.2" 29 | constraints = ">= 2.0.0" 30 | hashes = [ 31 | "h1:ocyv0lvfyvzW4krenxV5CL4Jq5DiA3EUfoy8DR6zFMw=", 32 | "zh:2487e498736ed90f53de8f66fe2b8c05665b9f8ff1506f751c5ee227c7f457d1", 33 | "zh:3d8627d142942336cf65eea6eb6403692f47e9072ff3fa11c3f774a3b93130b3", 34 | "zh:434b643054aeafb5df28d5529b72acc20c6f5ded24decad73b98657af2b53f4f", 35 | "zh:436aa6c2b07d82aa6a9dd746a3e3a627f72787c27c80552ceda6dc52d01f4b6f", 36 | "zh:458274c5aabe65ef4dbd61d43ce759287788e35a2da004e796373f88edcaa422", 37 | "zh:54bc70fa6fb7da33292ae4d9ceef5398d637c7373e729ed4fce59bd7b8d67372", 38 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 39 | "zh:893ba267e18749c1a956b69be569f0d7bc043a49c3a0eb4d0d09a8e8b2ca3136", 40 | "zh:95493b7517bce116f75cdd4c63b7c82a9d0d48ec2ef2f5eb836d262ef96d0aa7", 41 | "zh:9ae21ab393be52e3e84e5cce0ef20e690d21f6c10ade7d9d9d22b39851bfeddc", 42 | "zh:cc3b01ac2472e6d59358d54d5e4945032efbc8008739a6d4946ca1b621a16040", 43 | "zh:f23bfe9758f06a1ec10ea3a81c9deedf3a7b42963568997d84a5153f35c5839a", 44 | ] 45 | } 46 | 47 | provider "registry.terraform.io/hashicorp/kubernetes" { 48 | version = "2.23.0" 49 | constraints = ">= 2.10.0" 50 | hashes = [ 51 | "h1:arTzD0XG/DswGCAx9JEttkSKe9RyyFW9W7UWcXF13dU=", 52 | "zh:10488a12525ed674359585f83e3ee5e74818b5c98e033798351678b21b2f7d89", 53 | "zh:1102ba5ca1a595f880e67102bbf999cc8b60203272a078a5b1e896d173f3f34b", 54 | "zh:1347cf958ed3f3f80b3c7b3e23ddda3d6c6573a81847a8ee92b7df231c238bf6", 55 | "zh:2cb18e9f5156bc1b1ee6bc580a709f7c2737d142722948f4a6c3c8efe757fa8d", 56 | "zh:5506aa6f28dcca2a265ccf8e34478b5ec2cb43b867fe6d93b0158f01590fdadd", 57 | "zh:6217a20686b631b1dcb448ee4bc795747ebc61b56fbe97a1ad51f375ebb0d996", 58 | "zh:8accf916c00579c22806cb771e8909b349ffb7eb29d9c5468d0a3f3166c7a84a", 59 | "zh:9379b0b54a0fa030b19c7b9356708ec8489e194c3b5e978df2d31368563308e5", 60 | "zh:aa99c580890691036c2931841e88e7ee80d59ae52289c8c2c28ea0ac23e31520", 61 | "zh:c57376d169875990ac68664d227fb69cd0037b92d0eba6921d757c3fd1879080", 62 | "zh:e6068e3f94f6943b5586557b73f109debe19d1a75ca9273a681d22d1ce066579", 63 | "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", 64 | ] 65 | } 66 | 67 | provider "registry.terraform.io/hashicorp/time" { 68 | version = "0.9.1" 69 | constraints = ">= 0.9.0" 70 | hashes = [ 71 | "h1:VxyoYYOCaJGDmLz4TruZQTSfQhvwEcMxvcKclWdnpbs=", 72 | "zh:00a1476ecf18c735cc08e27bfa835c33f8ac8fa6fa746b01cd3bcbad8ca84f7f", 73 | "zh:3007f8fc4a4f8614c43e8ef1d4b0c773a5de1dcac50e701d8abc9fdc8fcb6bf5", 74 | "zh:5f79d0730fdec8cb148b277de3f00485eff3e9cf1ff47fb715b1c969e5bbd9d4", 75 | "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", 76 | "zh:8c8094689a2bed4bb597d24a418bbbf846e15507f08be447d0a5acea67c2265a", 77 | "zh:a6d9206e95d5681229429b406bc7a9ba4b2d9b67470bda7df88fa161508ace57", 78 | "zh:aa299ec058f23ebe68976c7581017de50da6204883950de228ed9246f309e7f1", 79 | "zh:b129f00f45fba1991db0aa954a6ba48d90f64a738629119bfb8e9a844b66e80b", 80 | "zh:ef6cecf5f50cda971c1b215847938ced4cb4a30a18095509c068643b14030b00", 81 | "zh:f1f46a4f6c65886d2dd27b66d92632232adc64f92145bf8403fe64d5ffa5caea", 82 | "zh:f79d6155cda7d559c60d74883a24879a01c4d5f6fd7e8d1e3250f3cd215fb904", 83 | "zh:fd59fa73074805c3575f08cd627eef7acda14ab6dac2c135a66e7a38d262201c", 84 | ] 85 | } 86 | 87 | provider "registry.terraform.io/hashicorp/tls" { 88 | version = "4.0.4" 89 | constraints = ">= 3.0.0" 90 | hashes = [ 91 | "h1:GZcFizg5ZT2VrpwvxGBHQ/hO9r6g0vYdQqx3bFD3anY=", 92 | "zh:23671ed83e1fcf79745534841e10291bbf34046b27d6e68a5d0aab77206f4a55", 93 | "zh:45292421211ffd9e8e3eb3655677700e3c5047f71d8f7650d2ce30242335f848", 94 | "zh:59fedb519f4433c0fdb1d58b27c210b27415fddd0cd73c5312530b4309c088be", 95 | "zh:5a8eec2409a9ff7cd0758a9d818c74bcba92a240e6c5e54b99df68fff312bbd5", 96 | "zh:5e6a4b39f3171f53292ab88058a59e64825f2b842760a4869e64dc1dc093d1fe", 97 | "zh:810547d0bf9311d21c81cc306126d3547e7bd3f194fc295836acf164b9f8424e", 98 | "zh:824a5f3617624243bed0259d7dd37d76017097dc3193dac669be342b90b2ab48", 99 | "zh:9361ccc7048be5dcbc2fafe2d8216939765b3160bd52734f7a9fd917a39ecbd8", 100 | "zh:aa02ea625aaf672e649296bce7580f62d724268189fe9ad7c1b36bb0fa12fa60", 101 | "zh:c71b4cd40d6ec7815dfeefd57d88bc592c0c42f5e5858dcc88245d371b4b8b1e", 102 | "zh:dabcd52f36b43d250a3d71ad7abfa07b5622c69068d989e60b79b2bb4f220316", 103 | "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", 104 | ] 105 | } 106 | -------------------------------------------------------------------------------- /internal/node/explorer.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "os" 7 | "strings" 8 | 9 | "github.com/doitintl/kubeip/internal/types" 10 | "github.com/pkg/errors" 11 | v1 "k8s.io/api/core/v1" 12 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 13 | "k8s.io/client-go/kubernetes" 14 | ) 15 | 16 | const ( 17 | minProviderIDTokens = 2 18 | podInfoDir = "/etc/podinfo/" 19 | awsPoolLabel = "eks.amazonaws.com/nodegroup" 20 | azurePoolLabel = "node.kubernetes.io/instancegroup" 21 | gcpPoolLabel = "cloud.google.com/gke-nodepool" 22 | ociPoolAnnotation = "oci.oraclecloud.com/node-pool-id" 23 | regionLabel = "topology.kubernetes.io/region" 24 | zoneLabel = "topology.kubernetes.io/zone" 25 | ) 26 | 27 | type Explorer interface { 28 | GetNode(ctx context.Context, nodeName string) (*types.Node, error) 29 | } 30 | 31 | type explorer struct { 32 | client kubernetes.Interface 33 | } 34 | 35 | func getNodeName(file string) (string, error) { 36 | // get node name from file 37 | nodeName, err := os.ReadFile(file) 38 | if err != nil { 39 | return "", errors.Wrapf(err, "failed to read %s", file) 40 | } 41 | return string(nodeName), nil 42 | } 43 | 44 | func NewExplorer(client kubernetes.Interface) Explorer { 45 | return &explorer{ 46 | client: client, 47 | } 48 | } 49 | 50 | func getCloudProvider(providerID string) (types.CloudProvider, error) { 51 | if strings.HasPrefix(providerID, "aws://") { 52 | return types.CloudProviderAWS, nil 53 | } 54 | if strings.HasPrefix(providerID, "azure://") { 55 | return types.CloudProviderAzure, nil 56 | } 57 | if strings.HasPrefix(providerID, "gce://") { 58 | return types.CloudProviderGCP, nil 59 | } 60 | if strings.HasPrefix(providerID, "oci") { 61 | return types.CloudProviderOCI, nil 62 | } 63 | return "", errors.Errorf("unsupported provider ID: %s", providerID) 64 | } 65 | 66 | func getInstance(providerID string) (string, error) { 67 | if providerID == "" { 68 | return "", errors.Errorf("failed to get instance ID, provider ID is empty") 69 | } 70 | 71 | // In case of OCI, the provider ID is the instance ID 72 | if strings.HasPrefix(providerID, "oci") { 73 | return providerID, nil 74 | } 75 | 76 | s := strings.Split(providerID, "/") 77 | if len(s) < minProviderIDTokens { 78 | return "", errors.Errorf("failed to get instance ID") 79 | } 80 | return s[len(s)-1], nil 81 | } 82 | 83 | func getNodePool(providerID types.CloudProvider, node *v1.Node) (string, error) { 84 | if node == nil { 85 | return "", errors.Errorf("node info is nil") 86 | } 87 | labels := node.Labels 88 | annotations := node.Annotations 89 | var ok bool 90 | var pool string 91 | if providerID == types.CloudProviderAWS { 92 | pool, ok = labels[awsPoolLabel] 93 | } else if providerID == types.CloudProviderAzure { 94 | pool, ok = labels[azurePoolLabel] 95 | } else if providerID == types.CloudProviderGCP { 96 | pool, ok = labels[gcpPoolLabel] 97 | } else if providerID == types.CloudProviderOCI { 98 | pool, ok = annotations[ociPoolAnnotation] 99 | } else { 100 | return "", errors.Errorf("unsupported cloud provider: %s", providerID) 101 | } 102 | if !ok { 103 | return "", errors.Errorf("failed to get node pool") 104 | } 105 | return pool, nil 106 | } 107 | 108 | func getAddresses(addresses []v1.NodeAddress) ([]net.IP, []net.IP, error) { 109 | var externalIPs []net.IP 110 | var internalIPs []net.IP 111 | for _, address := range addresses { 112 | if address.Type != v1.NodeExternalIP && address.Type != v1.NodeInternalIP { 113 | continue 114 | } 115 | ip := net.ParseIP(address.Address) 116 | if ip == nil { 117 | return nil, nil, errors.Errorf("failed to parse IP address: %s", address.Address) 118 | } 119 | if address.Type == v1.NodeExternalIP { 120 | externalIPs = append(externalIPs, ip) 121 | } else if address.Type == v1.NodeInternalIP { 122 | internalIPs = append(internalIPs, ip) 123 | } 124 | } 125 | return externalIPs, internalIPs, nil 126 | } 127 | 128 | // GetNode returns the node object 129 | func (d *explorer) GetNode(ctx context.Context, nodeName string) (*types.Node, error) { 130 | if d.client == nil { 131 | return nil, errors.Errorf("kubernetes client is nil") 132 | } 133 | 134 | // get node name from downward API if nodeName is empty 135 | if nodeName == "" { 136 | var err error 137 | nodeName, err = getNodeName(podInfoDir + "nodeName") 138 | if err != nil { 139 | return nil, errors.Wrap(err, "failed to get node name from downward API") 140 | } 141 | } 142 | 143 | // get node object from API server 144 | n, err := d.client.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{}) 145 | if err != nil { 146 | return nil, errors.Wrap(err, "failed to get kubernetes node") 147 | } 148 | 149 | // get cloud provider from node spec 150 | cloudProvider, err := getCloudProvider(n.Spec.ProviderID) 151 | if err != nil { 152 | return nil, errors.Wrap(err, "failed to get cloud provider") 153 | } 154 | 155 | // get instance ID from provider ID 156 | instance, err := getInstance(n.Spec.ProviderID) 157 | if err != nil { 158 | return nil, errors.Wrap(err, "failed to get instance ID") 159 | } 160 | 161 | // get node region from node labels 162 | region, ok := n.Labels[regionLabel] 163 | if !ok { 164 | return nil, errors.Errorf("failed to get node region") 165 | } 166 | 167 | // get node zone from node labels 168 | zone, ok := n.Labels[zoneLabel] 169 | if !ok { 170 | return nil, errors.Errorf("failed to get node zone") 171 | } 172 | 173 | // get node pool from node 174 | pool, err := getNodePool(cloudProvider, n) 175 | if err != nil { 176 | return nil, errors.Wrap(err, "failed to get node pool") 177 | } 178 | 179 | // get node addresses 180 | externalIPs, internalIPs, err := getAddresses(n.Status.Addresses) 181 | if err != nil { 182 | return nil, errors.Wrap(err, "failed to get node addresses") 183 | } 184 | 185 | return &types.Node{ 186 | Name: nodeName, 187 | Instance: instance, 188 | Cloud: cloudProvider, 189 | Region: region, 190 | Zone: zone, 191 | Pool: pool, 192 | ExternalIPs: externalIPs, 193 | InternalIPs: internalIPs, 194 | }, nil 195 | } 196 | -------------------------------------------------------------------------------- /mocks/cloud/ListCall.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | cloud "github.com/doitintl/kubeip/internal/cloud" 7 | compute "google.golang.org/api/compute/v1" 8 | 9 | mock "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | // ListCall is an autogenerated mock type for the ListCall type 13 | type ListCall struct { 14 | mock.Mock 15 | } 16 | 17 | type ListCall_Expecter struct { 18 | mock *mock.Mock 19 | } 20 | 21 | func (_m *ListCall) EXPECT() *ListCall_Expecter { 22 | return &ListCall_Expecter{mock: &_m.Mock} 23 | } 24 | 25 | // Do provides a mock function with given fields: 26 | func (_m *ListCall) Do() (*compute.AddressList, error) { 27 | ret := _m.Called() 28 | 29 | var r0 *compute.AddressList 30 | var r1 error 31 | if rf, ok := ret.Get(0).(func() (*compute.AddressList, error)); ok { 32 | return rf() 33 | } 34 | if rf, ok := ret.Get(0).(func() *compute.AddressList); ok { 35 | r0 = rf() 36 | } else { 37 | if ret.Get(0) != nil { 38 | r0 = ret.Get(0).(*compute.AddressList) 39 | } 40 | } 41 | 42 | if rf, ok := ret.Get(1).(func() error); ok { 43 | r1 = rf() 44 | } else { 45 | r1 = ret.Error(1) 46 | } 47 | 48 | return r0, r1 49 | } 50 | 51 | // ListCall_Do_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Do' 52 | type ListCall_Do_Call struct { 53 | *mock.Call 54 | } 55 | 56 | // Do is a helper method to define mock.On call 57 | func (_e *ListCall_Expecter) Do() *ListCall_Do_Call { 58 | return &ListCall_Do_Call{Call: _e.mock.On("Do")} 59 | } 60 | 61 | func (_c *ListCall_Do_Call) Run(run func()) *ListCall_Do_Call { 62 | _c.Call.Run(func(args mock.Arguments) { 63 | run() 64 | }) 65 | return _c 66 | } 67 | 68 | func (_c *ListCall_Do_Call) Return(_a0 *compute.AddressList, _a1 error) *ListCall_Do_Call { 69 | _c.Call.Return(_a0, _a1) 70 | return _c 71 | } 72 | 73 | func (_c *ListCall_Do_Call) RunAndReturn(run func() (*compute.AddressList, error)) *ListCall_Do_Call { 74 | _c.Call.Return(run) 75 | return _c 76 | } 77 | 78 | // Filter provides a mock function with given fields: filter 79 | func (_m *ListCall) Filter(filter string) cloud.ListCall { 80 | ret := _m.Called(filter) 81 | 82 | var r0 cloud.ListCall 83 | if rf, ok := ret.Get(0).(func(string) cloud.ListCall); ok { 84 | r0 = rf(filter) 85 | } else { 86 | if ret.Get(0) != nil { 87 | r0 = ret.Get(0).(cloud.ListCall) 88 | } 89 | } 90 | 91 | return r0 92 | } 93 | 94 | // ListCall_Filter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Filter' 95 | type ListCall_Filter_Call struct { 96 | *mock.Call 97 | } 98 | 99 | // Filter is a helper method to define mock.On call 100 | // - filter string 101 | func (_e *ListCall_Expecter) Filter(filter interface{}) *ListCall_Filter_Call { 102 | return &ListCall_Filter_Call{Call: _e.mock.On("Filter", filter)} 103 | } 104 | 105 | func (_c *ListCall_Filter_Call) Run(run func(filter string)) *ListCall_Filter_Call { 106 | _c.Call.Run(func(args mock.Arguments) { 107 | run(args[0].(string)) 108 | }) 109 | return _c 110 | } 111 | 112 | func (_c *ListCall_Filter_Call) Return(_a0 cloud.ListCall) *ListCall_Filter_Call { 113 | _c.Call.Return(_a0) 114 | return _c 115 | } 116 | 117 | func (_c *ListCall_Filter_Call) RunAndReturn(run func(string) cloud.ListCall) *ListCall_Filter_Call { 118 | _c.Call.Return(run) 119 | return _c 120 | } 121 | 122 | // OrderBy provides a mock function with given fields: orderBy 123 | func (_m *ListCall) OrderBy(orderBy string) cloud.ListCall { 124 | ret := _m.Called(orderBy) 125 | 126 | var r0 cloud.ListCall 127 | if rf, ok := ret.Get(0).(func(string) cloud.ListCall); ok { 128 | r0 = rf(orderBy) 129 | } else { 130 | if ret.Get(0) != nil { 131 | r0 = ret.Get(0).(cloud.ListCall) 132 | } 133 | } 134 | 135 | return r0 136 | } 137 | 138 | // ListCall_OrderBy_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OrderBy' 139 | type ListCall_OrderBy_Call struct { 140 | *mock.Call 141 | } 142 | 143 | // OrderBy is a helper method to define mock.On call 144 | // - orderBy string 145 | func (_e *ListCall_Expecter) OrderBy(orderBy interface{}) *ListCall_OrderBy_Call { 146 | return &ListCall_OrderBy_Call{Call: _e.mock.On("OrderBy", orderBy)} 147 | } 148 | 149 | func (_c *ListCall_OrderBy_Call) Run(run func(orderBy string)) *ListCall_OrderBy_Call { 150 | _c.Call.Run(func(args mock.Arguments) { 151 | run(args[0].(string)) 152 | }) 153 | return _c 154 | } 155 | 156 | func (_c *ListCall_OrderBy_Call) Return(_a0 cloud.ListCall) *ListCall_OrderBy_Call { 157 | _c.Call.Return(_a0) 158 | return _c 159 | } 160 | 161 | func (_c *ListCall_OrderBy_Call) RunAndReturn(run func(string) cloud.ListCall) *ListCall_OrderBy_Call { 162 | _c.Call.Return(run) 163 | return _c 164 | } 165 | 166 | // PageToken provides a mock function with given fields: pageToken 167 | func (_m *ListCall) PageToken(pageToken string) cloud.ListCall { 168 | ret := _m.Called(pageToken) 169 | 170 | var r0 cloud.ListCall 171 | if rf, ok := ret.Get(0).(func(string) cloud.ListCall); ok { 172 | r0 = rf(pageToken) 173 | } else { 174 | if ret.Get(0) != nil { 175 | r0 = ret.Get(0).(cloud.ListCall) 176 | } 177 | } 178 | 179 | return r0 180 | } 181 | 182 | // ListCall_PageToken_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PageToken' 183 | type ListCall_PageToken_Call struct { 184 | *mock.Call 185 | } 186 | 187 | // PageToken is a helper method to define mock.On call 188 | // - pageToken string 189 | func (_e *ListCall_Expecter) PageToken(pageToken interface{}) *ListCall_PageToken_Call { 190 | return &ListCall_PageToken_Call{Call: _e.mock.On("PageToken", pageToken)} 191 | } 192 | 193 | func (_c *ListCall_PageToken_Call) Run(run func(pageToken string)) *ListCall_PageToken_Call { 194 | _c.Call.Run(func(args mock.Arguments) { 195 | run(args[0].(string)) 196 | }) 197 | return _c 198 | } 199 | 200 | func (_c *ListCall_PageToken_Call) Return(_a0 cloud.ListCall) *ListCall_PageToken_Call { 201 | _c.Call.Return(_a0) 202 | return _c 203 | } 204 | 205 | func (_c *ListCall_PageToken_Call) RunAndReturn(run func(string) cloud.ListCall) *ListCall_PageToken_Call { 206 | _c.Call.Return(run) 207 | return _c 208 | } 209 | 210 | // NewListCall creates a new instance of ListCall. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 211 | // The first argument is typically a *testing.T value. 212 | func NewListCall(t interface { 213 | mock.TestingT 214 | Cleanup(func()) 215 | }) *ListCall { 216 | mock := &ListCall{} 217 | mock.Mock.Test(t) 218 | 219 | t.Cleanup(func() { mock.AssertExpectations(t) }) 220 | 221 | return mock 222 | } 223 | -------------------------------------------------------------------------------- /mocks/address/internalAssigner.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | compute "google.golang.org/api/compute/v1" 9 | 10 | mock "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | // InternalAssigner is an autogenerated mock type for the internalAssigner type 14 | type InternalAssigner struct { 15 | mock.Mock 16 | } 17 | 18 | type InternalAssigner_Expecter struct { 19 | mock *mock.Mock 20 | } 21 | 22 | func (_m *InternalAssigner) EXPECT() *InternalAssigner_Expecter { 23 | return &InternalAssigner_Expecter{mock: &_m.Mock} 24 | } 25 | 26 | // AddInstanceAddress provides a mock function with given fields: ctx, instance, zone, _a3 27 | func (_m *InternalAssigner) AddInstanceAddress(ctx context.Context, instance *compute.Instance, zone string, _a3 *compute.Address) error { 28 | ret := _m.Called(ctx, instance, zone, _a3) 29 | 30 | var r0 error 31 | if rf, ok := ret.Get(0).(func(context.Context, *compute.Instance, string, *compute.Address) error); ok { 32 | r0 = rf(ctx, instance, zone, _a3) 33 | } else { 34 | r0 = ret.Error(0) 35 | } 36 | 37 | return r0 38 | } 39 | 40 | // InternalAssigner_AddInstanceAddress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddInstanceAddress' 41 | type InternalAssigner_AddInstanceAddress_Call struct { 42 | *mock.Call 43 | } 44 | 45 | // AddInstanceAddress is a helper method to define mock.On call 46 | // - ctx context.Context 47 | // - instance *compute.Instance 48 | // - zone string 49 | // - _a3 *compute.Address 50 | func (_e *InternalAssigner_Expecter) AddInstanceAddress(ctx interface{}, instance interface{}, zone interface{}, _a3 interface{}) *InternalAssigner_AddInstanceAddress_Call { 51 | return &InternalAssigner_AddInstanceAddress_Call{Call: _e.mock.On("AddInstanceAddress", ctx, instance, zone, _a3)} 52 | } 53 | 54 | func (_c *InternalAssigner_AddInstanceAddress_Call) Run(run func(ctx context.Context, instance *compute.Instance, zone string, _a3 *compute.Address)) *InternalAssigner_AddInstanceAddress_Call { 55 | _c.Call.Run(func(args mock.Arguments) { 56 | run(args[0].(context.Context), args[1].(*compute.Instance), args[2].(string), args[3].(*compute.Address)) 57 | }) 58 | return _c 59 | } 60 | 61 | func (_c *InternalAssigner_AddInstanceAddress_Call) Return(_a0 error) *InternalAssigner_AddInstanceAddress_Call { 62 | _c.Call.Return(_a0) 63 | return _c 64 | } 65 | 66 | func (_c *InternalAssigner_AddInstanceAddress_Call) RunAndReturn(run func(context.Context, *compute.Instance, string, *compute.Address) error) *InternalAssigner_AddInstanceAddress_Call { 67 | _c.Call.Return(run) 68 | return _c 69 | } 70 | 71 | // CheckAddressAssigned provides a mock function with given fields: region, addressName 72 | func (_m *InternalAssigner) CheckAddressAssigned(region string, addressName string) (bool, error) { 73 | ret := _m.Called(region, addressName) 74 | 75 | var r0 bool 76 | var r1 error 77 | if rf, ok := ret.Get(0).(func(string, string) (bool, error)); ok { 78 | return rf(region, addressName) 79 | } 80 | if rf, ok := ret.Get(0).(func(string, string) bool); ok { 81 | r0 = rf(region, addressName) 82 | } else { 83 | r0 = ret.Get(0).(bool) 84 | } 85 | 86 | if rf, ok := ret.Get(1).(func(string, string) error); ok { 87 | r1 = rf(region, addressName) 88 | } else { 89 | r1 = ret.Error(1) 90 | } 91 | 92 | return r0, r1 93 | } 94 | 95 | // InternalAssigner_CheckAddressAssigned_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckAddressAssigned' 96 | type InternalAssigner_CheckAddressAssigned_Call struct { 97 | *mock.Call 98 | } 99 | 100 | // CheckAddressAssigned is a helper method to define mock.On call 101 | // - region string 102 | // - addressName string 103 | func (_e *InternalAssigner_Expecter) CheckAddressAssigned(region interface{}, addressName interface{}) *InternalAssigner_CheckAddressAssigned_Call { 104 | return &InternalAssigner_CheckAddressAssigned_Call{Call: _e.mock.On("CheckAddressAssigned", region, addressName)} 105 | } 106 | 107 | func (_c *InternalAssigner_CheckAddressAssigned_Call) Run(run func(region string, addressName string)) *InternalAssigner_CheckAddressAssigned_Call { 108 | _c.Call.Run(func(args mock.Arguments) { 109 | run(args[0].(string), args[1].(string)) 110 | }) 111 | return _c 112 | } 113 | 114 | func (_c *InternalAssigner_CheckAddressAssigned_Call) Return(_a0 bool, _a1 error) *InternalAssigner_CheckAddressAssigned_Call { 115 | _c.Call.Return(_a0, _a1) 116 | return _c 117 | } 118 | 119 | func (_c *InternalAssigner_CheckAddressAssigned_Call) RunAndReturn(run func(string, string) (bool, error)) *InternalAssigner_CheckAddressAssigned_Call { 120 | _c.Call.Return(run) 121 | return _c 122 | } 123 | 124 | // DeleteInstanceAddress provides a mock function with given fields: ctx, instance, zone 125 | func (_m *InternalAssigner) DeleteInstanceAddress(ctx context.Context, instance *compute.Instance, zone string) error { 126 | ret := _m.Called(ctx, instance, zone) 127 | 128 | var r0 error 129 | if rf, ok := ret.Get(0).(func(context.Context, *compute.Instance, string) error); ok { 130 | r0 = rf(ctx, instance, zone) 131 | } else { 132 | r0 = ret.Error(0) 133 | } 134 | 135 | return r0 136 | } 137 | 138 | // InternalAssigner_DeleteInstanceAddress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteInstanceAddress' 139 | type InternalAssigner_DeleteInstanceAddress_Call struct { 140 | *mock.Call 141 | } 142 | 143 | // DeleteInstanceAddress is a helper method to define mock.On call 144 | // - ctx context.Context 145 | // - instance *compute.Instance 146 | // - zone string 147 | func (_e *InternalAssigner_Expecter) DeleteInstanceAddress(ctx interface{}, instance interface{}, zone interface{}) *InternalAssigner_DeleteInstanceAddress_Call { 148 | return &InternalAssigner_DeleteInstanceAddress_Call{Call: _e.mock.On("DeleteInstanceAddress", ctx, instance, zone)} 149 | } 150 | 151 | func (_c *InternalAssigner_DeleteInstanceAddress_Call) Run(run func(ctx context.Context, instance *compute.Instance, zone string)) *InternalAssigner_DeleteInstanceAddress_Call { 152 | _c.Call.Run(func(args mock.Arguments) { 153 | run(args[0].(context.Context), args[1].(*compute.Instance), args[2].(string)) 154 | }) 155 | return _c 156 | } 157 | 158 | func (_c *InternalAssigner_DeleteInstanceAddress_Call) Return(_a0 error) *InternalAssigner_DeleteInstanceAddress_Call { 159 | _c.Call.Return(_a0) 160 | return _c 161 | } 162 | 163 | func (_c *InternalAssigner_DeleteInstanceAddress_Call) RunAndReturn(run func(context.Context, *compute.Instance, string) error) *InternalAssigner_DeleteInstanceAddress_Call { 164 | _c.Call.Return(run) 165 | return _c 166 | } 167 | 168 | // NewInternalAssigner creates a new instance of InternalAssigner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 169 | // The first argument is typically a *testing.T value. 170 | func NewInternalAssigner(t interface { 171 | mock.TestingT 172 | Cleanup(func()) 173 | }) *InternalAssigner { 174 | mock := &InternalAssigner{} 175 | mock.Mock.Test(t) 176 | 177 | t.Cleanup(func() { mock.AssertExpectations(t) }) 178 | 179 | return mock 180 | } 181 | -------------------------------------------------------------------------------- /internal/node/tainter_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/doitintl/kubeip/internal/types" 9 | v1 "k8s.io/api/core/v1" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/client-go/kubernetes/fake" 12 | ) 13 | 14 | func Test_deleteTaintsByKey(t *testing.T) { 15 | tests := []struct { 16 | name string 17 | taints []v1.Taint 18 | taintKey string 19 | want []v1.Taint 20 | wantDidDelete bool 21 | }{ 22 | { 23 | name: "taints contains taintKey", 24 | taints: []v1.Taint{ 25 | { 26 | Key: "taint1", 27 | Value: "one", 28 | }, 29 | { 30 | Key: "taint2", 31 | Value: "two", 32 | }, 33 | }, 34 | taintKey: "taint2", 35 | want: []v1.Taint{ 36 | { 37 | Key: "taint1", 38 | Value: "one", 39 | }, 40 | }, 41 | wantDidDelete: true, 42 | }, 43 | { 44 | name: "taint does not contain taintKey", 45 | taints: []v1.Taint{ 46 | { 47 | Key: "taint1", 48 | Value: "one", 49 | }, 50 | }, 51 | taintKey: "taint2", 52 | want: []v1.Taint{ 53 | { 54 | Key: "taint1", 55 | Value: "one", 56 | }, 57 | }, 58 | wantDidDelete: false, 59 | }, 60 | } 61 | 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | got, gotDidDelete := deleteTaintsByKey(tt.taints, tt.taintKey) 65 | 66 | if !reflect.DeepEqual(got, tt.want) { 67 | t.Errorf("deleteTaintsByKey() got = %v, want %v", got, tt.want) 68 | } 69 | 70 | if gotDidDelete != tt.wantDidDelete { 71 | t.Errorf("deleteTaintsByKey() gotDidDelete = %v, want %v", gotDidDelete, tt.wantDidDelete) 72 | } 73 | }) 74 | } 75 | } 76 | 77 | func Test_tainter_RemoveTaintKey(t *testing.T) { 78 | type fields struct { 79 | client *fake.Clientset 80 | } 81 | type args struct { 82 | node *types.Node 83 | taintKey string 84 | } 85 | 86 | tests := []struct { 87 | name string 88 | fields *fields 89 | args args 90 | want bool 91 | wantErr bool 92 | validateNode func(t *testing.T, node *v1.Node) 93 | }{ 94 | { 95 | name: "remove taint key", 96 | fields: &fields{ 97 | client: fake.NewSimpleClientset(&v1.Node{ 98 | ObjectMeta: metav1.ObjectMeta{ 99 | Name: "node1", 100 | }, 101 | Spec: v1.NodeSpec{ 102 | Taints: []v1.Taint{ 103 | { 104 | Key: "taint1", 105 | Value: "true", 106 | Effect: "NoSchedule", 107 | }, 108 | { 109 | Key: "taint2", 110 | Value: "two", 111 | Effect: "NoSchedule", 112 | }, 113 | }, 114 | }, 115 | }), 116 | }, 117 | args: args{ 118 | node: &types.Node{Name: "node1"}, 119 | taintKey: "taint1", 120 | }, 121 | want: true, 122 | wantErr: false, 123 | validateNode: func(t *testing.T, node *v1.Node) { 124 | if node.ObjectMeta.Name != "node1" { 125 | t.Errorf("RemoveTaintKey() node.ObjectMeta.Name = %v, want node1", node.ObjectMeta.Name) 126 | } 127 | 128 | if len(node.Spec.Taints) != 1 { 129 | t.Errorf("RemoveTaintKey() node.Spec.Taints = %v, want 1", node.Spec.Taints) 130 | } 131 | 132 | if node.Spec.Taints[0].Key != "taint2" { 133 | t.Errorf("RemoveTaintKey() node.Spec.Taints[0].Key = %v, want taint2", node.Spec.Taints[0].Key) 134 | } 135 | }, 136 | }, 137 | { 138 | name: "only one taint key on node", 139 | fields: &fields{ 140 | client: fake.NewSimpleClientset(&v1.Node{ 141 | ObjectMeta: metav1.ObjectMeta{ 142 | Name: "node1", 143 | }, 144 | Spec: v1.NodeSpec{ 145 | Taints: []v1.Taint{ 146 | { 147 | Key: "taint1", 148 | Value: "true", 149 | Effect: "NoSchedule", 150 | }, 151 | }, 152 | }, 153 | }), 154 | }, 155 | args: args{ 156 | node: &types.Node{Name: "node1"}, 157 | taintKey: "taint1", 158 | }, 159 | want: true, 160 | wantErr: false, 161 | validateNode: func(t *testing.T, node *v1.Node) { 162 | if node.ObjectMeta.Name != "node1" { 163 | t.Errorf("RemoveTaintKey() node.ObjectMeta.Name = %v, want node1", node.ObjectMeta.Name) 164 | } 165 | 166 | if len(node.Spec.Taints) != 0 { 167 | t.Errorf("RemoveTaintKey() node.Spec.Taints = %v, want 0", node.Spec.Taints) 168 | } 169 | }, 170 | }, 171 | { 172 | name: "taint key not present on node", 173 | fields: &fields{ 174 | client: fake.NewSimpleClientset(&v1.Node{ 175 | ObjectMeta: metav1.ObjectMeta{ 176 | Name: "node1", 177 | }, 178 | Spec: v1.NodeSpec{ 179 | Taints: []v1.Taint{ 180 | { 181 | Key: "taint1", 182 | Value: "true", 183 | Effect: "NoSchedule", 184 | }, 185 | }, 186 | }, 187 | }), 188 | }, 189 | args: args{ 190 | node: &types.Node{Name: "node1"}, 191 | taintKey: "taint2", 192 | }, 193 | want: false, 194 | wantErr: false, 195 | validateNode: func(t *testing.T, node *v1.Node) { 196 | if node.ObjectMeta.Name != "node1" { 197 | t.Errorf("RemoveTaintKey() node.ObjectMeta.Name = %v, want node1", node.ObjectMeta.Name) 198 | } 199 | 200 | if len(node.Spec.Taints) != 1 { 201 | t.Errorf("RemoveTaintKey() node.Spec.Taints = %v, want 1", node.Spec.Taints) 202 | } 203 | 204 | if node.Spec.Taints[0].Key != "taint1" { 205 | t.Errorf("RemoveTaintKey() node.Spec.Taints[0].Key = %v, want taint1", node.Spec.Taints[0].Key) 206 | } 207 | }, 208 | }, 209 | { 210 | name: "no taints on node", 211 | fields: &fields{ 212 | client: fake.NewSimpleClientset(&v1.Node{ 213 | ObjectMeta: metav1.ObjectMeta{ 214 | Name: "node1", 215 | }, 216 | Spec: v1.NodeSpec{}, 217 | }), 218 | }, 219 | args: args{ 220 | node: &types.Node{Name: "node1"}, 221 | taintKey: "taint1", 222 | }, 223 | want: false, 224 | wantErr: false, 225 | validateNode: func(t *testing.T, node *v1.Node) { 226 | if node.ObjectMeta.Name != "node1" { 227 | t.Errorf("RemoveTaintKey() node.ObjectMeta.Name = %v, want node1", node.ObjectMeta.Name) 228 | } 229 | 230 | if len(node.Spec.Taints) != 0 { 231 | t.Errorf("RemoveTaintKey() node.Spec.Taints = %v, want 0", node.Spec.Taints) 232 | } 233 | }, 234 | }, 235 | { 236 | name: "node not found", 237 | fields: &fields{ 238 | client: fake.NewSimpleClientset(), 239 | }, 240 | args: args{ 241 | node: &types.Node{Name: "node1"}, 242 | taintKey: "taint1", 243 | }, 244 | want: false, 245 | wantErr: true, 246 | validateNode: func(t *testing.T, node *v1.Node) { 247 | // no node to validate 248 | }, 249 | }, 250 | } 251 | 252 | for _, tt := range tests { 253 | t.Run(tt.name, func(t *testing.T) { 254 | ctx, cancel := context.WithCancel(context.Background()) 255 | defer cancel() 256 | 257 | tainter := NewTainter(tt.fields.client) 258 | got, err := tainter.RemoveTaintKey(ctx, tt.args.node, tt.args.taintKey) 259 | 260 | if (err != nil) != tt.wantErr { 261 | t.Errorf("RemoveTaintKey() error = %v, wantErr %v", err, tt.wantErr) 262 | } 263 | if got != tt.want { 264 | t.Errorf("RemoveTaintKey() got = %v, want %v", got, tt.want) 265 | } 266 | 267 | if !tt.wantErr { 268 | node, _ := tt.fields.client.CoreV1().Nodes().Get(ctx, tt.args.node.Name, metav1.GetOptions{}) 269 | tt.validateNode(t, node) 270 | } 271 | }) 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /examples/aws/eks.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | } 4 | 5 | module "vpc" { 6 | source = "terraform-aws-modules/vpc/aws" 7 | 8 | name = var.vpc_name 9 | cidr = var.vpc_cidr 10 | azs = var.availability_zones 11 | private_subnets = var.private_cidr_ranges 12 | public_subnets = var.public_cidr_ranges 13 | enable_nat_gateway = true 14 | single_nat_gateway = true 15 | enable_dns_hostnames = true 16 | map_public_ip_on_launch = true 17 | 18 | tags = { 19 | App = "kubeip" 20 | Env = "demo" 21 | } 22 | public_subnet_tags = { 23 | public = "true" 24 | environment = "demo" 25 | } 26 | private_subnet_tags = { 27 | public = "false" 28 | environment = "demo" 29 | } 30 | } 31 | 32 | module "eks" { 33 | source = "terraform-aws-modules/eks/aws" 34 | 35 | cluster_name = var.cluster_name 36 | cluster_version = var.kubernetes_version 37 | 38 | cluster_endpoint_public_access = true 39 | 40 | vpc_id = module.vpc.vpc_id 41 | subnet_ids = concat(module.vpc.private_subnets, module.vpc.public_subnets) 42 | 43 | eks_managed_node_groups = { 44 | eks_nodes_public = { 45 | desired_size = 3 46 | max_size = 5 47 | min_size = 1 48 | 49 | instance_types = ["t3a.small", "t3a.medium"] 50 | capacity_type = "SPOT" 51 | 52 | labels = { 53 | nodegroup = "public" 54 | kubeip = "use" 55 | } 56 | 57 | tags = { 58 | Name = "public-node-group" 59 | environment = "demo" 60 | public = "true" 61 | kubeip = "use" 62 | } 63 | 64 | subnet_ids = module.vpc.public_subnets 65 | } 66 | 67 | eks_nodes_private = { 68 | desired_size = 1 69 | max_size = 5 70 | min_size = 1 71 | 72 | instance_types = ["t3a.small", "t3a.medium"] 73 | capacity_type = "SPOT" 74 | 75 | labels = { 76 | nodegroup = "private" 77 | kubeip = "ignore" 78 | } 79 | 80 | tags = { 81 | Name = "private-node-group" 82 | environment = "demo" 83 | } 84 | 85 | subnet_ids = module.vpc.private_subnets 86 | } 87 | } 88 | } 89 | 90 | resource "aws_iam_policy" "kubeip-policy" { 91 | name = "kubeip-policy" 92 | description = "KubeIP required permissions" 93 | 94 | policy = jsonencode({ 95 | Version = "2012-10-17" 96 | Statement = [ 97 | { 98 | Action = [ 99 | "ec2:AssociateAddress", 100 | "ec2:DisassociateAddress", 101 | "ec2:DescribeInstances", 102 | "ec2:DescribeAddresses" 103 | ] 104 | Effect = "Allow" 105 | Resource = "*" 106 | }, 107 | ] 108 | }) 109 | } 110 | 111 | module "kubeip_eks_role" { 112 | source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" 113 | role_name = "kubeip-eks-role" 114 | 115 | role_policy_arns = { 116 | "kubeip-policy" = aws_iam_policy.kubeip-policy.arn 117 | } 118 | 119 | oidc_providers = { 120 | main = { 121 | provider_arn = module.eks.oidc_provider_arn 122 | namespace_service_accounts = ["kube-system:kubeip-service-account"] 123 | } 124 | } 125 | } 126 | 127 | # 3 elastic IPs in the same region 128 | resource "aws_eip" "kubeip" { 129 | // default EIP limit is 5 (make sure to increase it if you need more) 130 | count = 5 131 | 132 | tags = { 133 | Name = "kubeip-${count.index}" 134 | environment = "demo" 135 | kubeip = "reserved" 136 | } 137 | } 138 | 139 | data "aws_eks_cluster_auth" "kubeip_cluster_auth" { 140 | name = module.eks.cluster_name 141 | } 142 | 143 | provider "kubernetes" { 144 | host = module.eks.cluster_endpoint 145 | cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) 146 | token = data.aws_eks_cluster_auth.kubeip_cluster_auth.token 147 | } 148 | 149 | resource "kubernetes_service_account" "kubeip_service_account" { 150 | metadata { 151 | name = "kubeip-service-account" 152 | namespace = "kube-system" 153 | annotations = { 154 | "eks.amazonaws.com/role-arn" = module.kubeip_eks_role.iam_role_arn 155 | } 156 | } 157 | depends_on = [module.eks] 158 | } 159 | 160 | # Create cluster role with get node permission 161 | resource "kubernetes_cluster_role" "kubeip_cluster_role" { 162 | metadata { 163 | name = "kubeip-cluster-role" 164 | } 165 | rule { 166 | api_groups = ["*"] 167 | resources = ["nodes"] 168 | verbs = ["get"] 169 | } 170 | rule { 171 | api_groups = ["coordination.k8s.io"] 172 | resources = ["leases"] 173 | verbs = ["create", "delete", "get"] 174 | } 175 | depends_on = [ 176 | kubernetes_service_account.kubeip_service_account, 177 | module.eks 178 | ] 179 | } 180 | 181 | # Bind cluster role to kubeip service account 182 | resource "kubernetes_cluster_role_binding" "kubeip_cluster_role_binding" { 183 | metadata { 184 | name = "kubeip-cluster-role-binding" 185 | } 186 | role_ref { 187 | api_group = "rbac.authorization.k8s.io" 188 | kind = "ClusterRole" 189 | name = kubernetes_cluster_role.kubeip_cluster_role.metadata[0].name 190 | } 191 | subject { 192 | kind = "ServiceAccount" 193 | name = kubernetes_service_account.kubeip_service_account.metadata[0].name 194 | namespace = kubernetes_service_account.kubeip_service_account.metadata[0].namespace 195 | } 196 | depends_on = [ 197 | kubernetes_service_account.kubeip_service_account, 198 | kubernetes_cluster_role.kubeip_cluster_role 199 | ] 200 | } 201 | 202 | 203 | # Deploy KubeIP DaemonSet 204 | resource "kubernetes_daemonset" "kubeip_daemonset" { 205 | metadata { 206 | name = "kubeip-agent" 207 | namespace = "kube-system" 208 | labels = { 209 | app = "kubeip" 210 | } 211 | } 212 | spec { 213 | selector { 214 | match_labels = { 215 | app = "kubeip" 216 | } 217 | } 218 | strategy { 219 | type = "RollingUpdate" 220 | rolling_update { 221 | max_unavailable = 1 222 | } 223 | } 224 | template { 225 | metadata { 226 | labels = { 227 | app = "kubeip" 228 | } 229 | } 230 | spec { 231 | service_account_name = "kubeip-service-account" 232 | termination_grace_period_seconds = 30 233 | priority_class_name = "system-node-critical" 234 | toleration { 235 | effect = "NoSchedule" 236 | operator = "Exists" 237 | } 238 | toleration { 239 | effect = "NoExecute" 240 | operator = "Exists" 241 | } 242 | container { 243 | name = "kubeip-agent" 244 | image = "doitintl/kubeip-agent:${var.kubeip_version}" 245 | env { 246 | name = "NODE_NAME" 247 | value_from { 248 | field_ref { 249 | field_path = "spec.nodeName" 250 | } 251 | } 252 | } 253 | env { 254 | name = "FILTER" 255 | value = "Name=tag:kubeip,Values=reserved;Name=tag:environment,Values=demo" 256 | } 257 | env { 258 | name = "LOG_LEVEL" 259 | value = "debug" 260 | } 261 | resources { 262 | requests = { 263 | cpu = "10m" 264 | memory = "32Mi" 265 | } 266 | } 267 | } 268 | node_selector = { 269 | nodegroup = "public" 270 | kubeip = "use" 271 | } 272 | } 273 | } 274 | } 275 | depends_on = [kubernetes_service_account.kubeip_service_account] 276 | } 277 | -------------------------------------------------------------------------------- /mocks/cloud/AddressManager.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | mock "github.com/stretchr/testify/mock" 7 | compute "google.golang.org/api/compute/v1" 8 | ) 9 | 10 | // AddressManager is an autogenerated mock type for the AddressManager type 11 | type AddressManager struct { 12 | mock.Mock 13 | } 14 | 15 | type AddressManager_Expecter struct { 16 | mock *mock.Mock 17 | } 18 | 19 | func (_m *AddressManager) EXPECT() *AddressManager_Expecter { 20 | return &AddressManager_Expecter{mock: &_m.Mock} 21 | } 22 | 23 | // AddAccessConfig provides a mock function with given fields: project, zone, instance, networkInterface, fingerprint, accessconfig 24 | func (_m *AddressManager) AddAccessConfig(project string, zone string, instance string, networkInterface string, fingerprint string, accessconfig *compute.AccessConfig) (*compute.Operation, error) { 25 | ret := _m.Called(project, zone, instance, networkInterface, fingerprint, accessconfig) 26 | 27 | var r0 *compute.Operation 28 | var r1 error 29 | if rf, ok := ret.Get(0).(func(string, string, string, string, string, *compute.AccessConfig) (*compute.Operation, error)); ok { 30 | return rf(project, zone, instance, networkInterface, fingerprint, accessconfig) 31 | } 32 | if rf, ok := ret.Get(0).(func(string, string, string, string, string, *compute.AccessConfig) *compute.Operation); ok { 33 | r0 = rf(project, zone, instance, networkInterface, fingerprint, accessconfig) 34 | } else { 35 | if ret.Get(0) != nil { 36 | r0 = ret.Get(0).(*compute.Operation) 37 | } 38 | } 39 | 40 | if rf, ok := ret.Get(1).(func(string, string, string, string, string, *compute.AccessConfig) error); ok { 41 | r1 = rf(project, zone, instance, networkInterface, fingerprint, accessconfig) 42 | } else { 43 | r1 = ret.Error(1) 44 | } 45 | 46 | return r0, r1 47 | } 48 | 49 | // AddressManager_AddAccessConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddAccessConfig' 50 | type AddressManager_AddAccessConfig_Call struct { 51 | *mock.Call 52 | } 53 | 54 | // AddAccessConfig is a helper method to define mock.On call 55 | // - project string 56 | // - zone string 57 | // - instance string 58 | // - networkInterface string 59 | // - fingerprint string 60 | // - accessconfig *compute.AccessConfig 61 | func (_e *AddressManager_Expecter) AddAccessConfig(project interface{}, zone interface{}, instance interface{}, networkInterface interface{}, fingerprint interface{}, accessconfig interface{}) *AddressManager_AddAccessConfig_Call { 62 | return &AddressManager_AddAccessConfig_Call{Call: _e.mock.On("AddAccessConfig", project, zone, instance, networkInterface, fingerprint, accessconfig)} 63 | } 64 | 65 | func (_c *AddressManager_AddAccessConfig_Call) Run(run func(project string, zone string, instance string, networkInterface string, fingerprint string, accessconfig *compute.AccessConfig)) *AddressManager_AddAccessConfig_Call { 66 | _c.Call.Run(func(args mock.Arguments) { 67 | run(args[0].(string), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(*compute.AccessConfig)) 68 | }) 69 | return _c 70 | } 71 | 72 | func (_c *AddressManager_AddAccessConfig_Call) Return(_a0 *compute.Operation, _a1 error) *AddressManager_AddAccessConfig_Call { 73 | _c.Call.Return(_a0, _a1) 74 | return _c 75 | } 76 | 77 | func (_c *AddressManager_AddAccessConfig_Call) RunAndReturn(run func(string, string, string, string, string, *compute.AccessConfig) (*compute.Operation, error)) *AddressManager_AddAccessConfig_Call { 78 | _c.Call.Return(run) 79 | return _c 80 | } 81 | 82 | // DeleteAccessConfig provides a mock function with given fields: project, zone, instance, accessConfig, networkInterface, fingerprint 83 | func (_m *AddressManager) DeleteAccessConfig(project string, zone string, instance string, accessConfig string, networkInterface string, fingerprint string) (*compute.Operation, error) { 84 | ret := _m.Called(project, zone, instance, accessConfig, networkInterface, fingerprint) 85 | 86 | var r0 *compute.Operation 87 | var r1 error 88 | if rf, ok := ret.Get(0).(func(string, string, string, string, string, string) (*compute.Operation, error)); ok { 89 | return rf(project, zone, instance, accessConfig, networkInterface, fingerprint) 90 | } 91 | if rf, ok := ret.Get(0).(func(string, string, string, string, string, string) *compute.Operation); ok { 92 | r0 = rf(project, zone, instance, accessConfig, networkInterface, fingerprint) 93 | } else { 94 | if ret.Get(0) != nil { 95 | r0 = ret.Get(0).(*compute.Operation) 96 | } 97 | } 98 | 99 | if rf, ok := ret.Get(1).(func(string, string, string, string, string, string) error); ok { 100 | r1 = rf(project, zone, instance, accessConfig, networkInterface, fingerprint) 101 | } else { 102 | r1 = ret.Error(1) 103 | } 104 | 105 | return r0, r1 106 | } 107 | 108 | // AddressManager_DeleteAccessConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteAccessConfig' 109 | type AddressManager_DeleteAccessConfig_Call struct { 110 | *mock.Call 111 | } 112 | 113 | // DeleteAccessConfig is a helper method to define mock.On call 114 | // - project string 115 | // - zone string 116 | // - instance string 117 | // - accessConfig string 118 | // - networkInterface string 119 | // - fingerprint string 120 | func (_e *AddressManager_Expecter) DeleteAccessConfig(project interface{}, zone interface{}, instance interface{}, accessConfig interface{}, networkInterface interface{}, fingerprint interface{}) *AddressManager_DeleteAccessConfig_Call { 121 | return &AddressManager_DeleteAccessConfig_Call{Call: _e.mock.On("DeleteAccessConfig", project, zone, instance, accessConfig, networkInterface, fingerprint)} 122 | } 123 | 124 | func (_c *AddressManager_DeleteAccessConfig_Call) Run(run func(project string, zone string, instance string, accessConfig string, networkInterface string, fingerprint string)) *AddressManager_DeleteAccessConfig_Call { 125 | _c.Call.Run(func(args mock.Arguments) { 126 | run(args[0].(string), args[1].(string), args[2].(string), args[3].(string), args[4].(string), args[5].(string)) 127 | }) 128 | return _c 129 | } 130 | 131 | func (_c *AddressManager_DeleteAccessConfig_Call) Return(_a0 *compute.Operation, _a1 error) *AddressManager_DeleteAccessConfig_Call { 132 | _c.Call.Return(_a0, _a1) 133 | return _c 134 | } 135 | 136 | func (_c *AddressManager_DeleteAccessConfig_Call) RunAndReturn(run func(string, string, string, string, string, string) (*compute.Operation, error)) *AddressManager_DeleteAccessConfig_Call { 137 | _c.Call.Return(run) 138 | return _c 139 | } 140 | 141 | // GetAddress provides a mock function with given fields: project, region, name 142 | func (_m *AddressManager) GetAddress(project string, region string, name string) (*compute.Address, error) { 143 | ret := _m.Called(project, region, name) 144 | 145 | var r0 *compute.Address 146 | var r1 error 147 | if rf, ok := ret.Get(0).(func(string, string, string) (*compute.Address, error)); ok { 148 | return rf(project, region, name) 149 | } 150 | if rf, ok := ret.Get(0).(func(string, string, string) *compute.Address); ok { 151 | r0 = rf(project, region, name) 152 | } else { 153 | if ret.Get(0) != nil { 154 | r0 = ret.Get(0).(*compute.Address) 155 | } 156 | } 157 | 158 | if rf, ok := ret.Get(1).(func(string, string, string) error); ok { 159 | r1 = rf(project, region, name) 160 | } else { 161 | r1 = ret.Error(1) 162 | } 163 | 164 | return r0, r1 165 | } 166 | 167 | // AddressManager_GetAddress_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAddress' 168 | type AddressManager_GetAddress_Call struct { 169 | *mock.Call 170 | } 171 | 172 | // GetAddress is a helper method to define mock.On call 173 | // - project string 174 | // - region string 175 | // - name string 176 | func (_e *AddressManager_Expecter) GetAddress(project interface{}, region interface{}, name interface{}) *AddressManager_GetAddress_Call { 177 | return &AddressManager_GetAddress_Call{Call: _e.mock.On("GetAddress", project, region, name)} 178 | } 179 | 180 | func (_c *AddressManager_GetAddress_Call) Run(run func(project string, region string, name string)) *AddressManager_GetAddress_Call { 181 | _c.Call.Run(func(args mock.Arguments) { 182 | run(args[0].(string), args[1].(string), args[2].(string)) 183 | }) 184 | return _c 185 | } 186 | 187 | func (_c *AddressManager_GetAddress_Call) Return(_a0 *compute.Address, _a1 error) *AddressManager_GetAddress_Call { 188 | _c.Call.Return(_a0, _a1) 189 | return _c 190 | } 191 | 192 | func (_c *AddressManager_GetAddress_Call) RunAndReturn(run func(string, string, string) (*compute.Address, error)) *AddressManager_GetAddress_Call { 193 | _c.Call.Return(run) 194 | return _c 195 | } 196 | 197 | // NewAddressManager creates a new instance of AddressManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 198 | // The first argument is typically a *testing.T value. 199 | func NewAddressManager(t interface { 200 | mock.TestingT 201 | Cleanup(func()) 202 | }) *AddressManager { 203 | mock := &AddressManager{} 204 | mock.Mock.Test(t) 205 | 206 | t.Cleanup(func() { mock.AssertExpectations(t) }) 207 | 208 | return mock 209 | } 210 | -------------------------------------------------------------------------------- /examples/gcp/gke.tf: -------------------------------------------------------------------------------- 1 | # Save state to local file 2 | terraform { 3 | backend "local" { 4 | path = "terraform.tfstate" 5 | } 6 | } 7 | 8 | # Set the provider and credentials 9 | provider "google" { 10 | project = var.project_id 11 | region = var.region 12 | } 13 | 14 | # Create custom IAM Role 15 | resource "google_project_iam_custom_role" "kubeip_role" { 16 | role_id = "kubeip_role" 17 | title = "KubeIP Role" 18 | description = "KubeIP required permissions" 19 | stage = "GA" 20 | permissions = [ 21 | "compute.instances.addAccessConfig", 22 | "compute.instances.deleteAccessConfig", 23 | "compute.instances.get", 24 | "compute.addresses.get", 25 | "compute.addresses.list", 26 | "compute.addresses.use", 27 | "compute.zoneOperations.get", 28 | "compute.zoneOperations.list", 29 | "compute.subnetworks.useExternalIp", 30 | "compute.projects.get" 31 | ] 32 | } 33 | 34 | # Create custom IAM service account 35 | resource "google_service_account" "kubeip_service_account" { 36 | account_id = "kubeip-service-account" 37 | display_name = "KubeIP Service Account" 38 | } 39 | 40 | # Bind custom IAM Role to kubeip IAM service account 41 | resource "google_project_iam_member" "kubeip_role_binding" { 42 | role = google_project_iam_custom_role.kubeip_role.id 43 | member = "serviceAccount:${google_service_account.kubeip_service_account.email}" 44 | project = var.project_id 45 | } 46 | 47 | # Bind workload identity to kubeip IAM service account 48 | resource "google_service_account_iam_member" "kubeip_workload_identity_binding" { 49 | service_account_id = google_service_account.kubeip_service_account.name 50 | role = "roles/iam.workloadIdentityUser" 51 | member = "serviceAccount:${var.project_id}.svc.id.goog[kube-system/kubeip-service-account]" 52 | } 53 | 54 | # Create a VPC network 55 | resource "google_compute_network" "vpc" { 56 | name = var.vpc_name 57 | auto_create_subnetworks = false 58 | } 59 | 60 | # Create a public subnet 61 | resource "google_compute_subnetwork" "kubeip_subnet" { 62 | name = "kubeip-subnet" 63 | network = google_compute_network.vpc.id 64 | region = var.region 65 | ip_cidr_range = var.subnet_range 66 | stack_type = var.ipv6_support ? "IPV4_IPV6" : "IPV4_ONLY" 67 | ipv6_access_type = var.ipv6_support ? "EXTERNAL" : "" 68 | private_ip_google_access = true 69 | secondary_ip_range { 70 | range_name = var.services_range_name 71 | ip_cidr_range = var.services_range 72 | } 73 | secondary_ip_range { 74 | range_name = var.pods_range_name 75 | ip_cidr_range = var.pods_range 76 | } 77 | } 78 | 79 | # Create GKE cluster 80 | resource "google_container_cluster" "kubeip_cluster" { 81 | name = var.cluster_name 82 | location = var.region 83 | 84 | initial_node_count = 1 85 | remove_default_node_pool = true 86 | 87 | network = google_compute_network.vpc.id 88 | subnetwork = google_compute_subnetwork.kubeip_subnet.id 89 | datapath_provider = var.ipv6_support ? "ADVANCED_DATAPATH" : "LEGACY_DATAPATH" 90 | enable_l4_ilb_subsetting = true 91 | 92 | ip_allocation_policy { 93 | services_secondary_range_name = var.services_range_name 94 | cluster_secondary_range_name = var.pods_range_name 95 | stack_type = var.ipv6_support ? "IPV4_IPV6" : "IPV4" 96 | } 97 | 98 | # Enable Workload Identity 99 | workload_identity_config { 100 | workload_pool = "${var.project_id}.svc.id.goog" 101 | } 102 | } 103 | 104 | # Create node pools 105 | resource "google_container_node_pool" "public_node_pool" { 106 | name = "public-node-pool" 107 | location = google_container_cluster.kubeip_cluster.location 108 | cluster = google_container_cluster.kubeip_cluster.name 109 | initial_node_count = 1 110 | autoscaling { 111 | min_node_count = 1 112 | max_node_count = 2 113 | location_policy = "ANY" 114 | } 115 | node_config { 116 | machine_type = var.machine_type 117 | spot = true 118 | oauth_scopes = [ 119 | "https://www.googleapis.com/auth/logging.write", 120 | "https://www.googleapis.com/auth/monitoring", 121 | ] 122 | metadata = { 123 | disable-legacy-endpoints = "true" 124 | } 125 | workload_metadata_config { 126 | mode = "GKE_METADATA" 127 | } 128 | labels = { 129 | nodegroup = "public" 130 | kubeip = "use" 131 | } 132 | resource_labels = { 133 | environment = "demo" 134 | kubeip = "use" 135 | public = "true" 136 | } 137 | } 138 | } 139 | 140 | resource "google_container_node_pool" "private_node_pool" { 141 | name = "private-node-pool" 142 | location = google_container_cluster.kubeip_cluster.location 143 | cluster = google_container_cluster.kubeip_cluster.name 144 | initial_node_count = 1 145 | autoscaling { 146 | min_node_count = 1 147 | max_node_count = 2 148 | location_policy = "ANY" 149 | } 150 | node_config { 151 | machine_type = var.machine_type 152 | spot = true 153 | oauth_scopes = [ 154 | "https://www.googleapis.com/auth/logging.write", 155 | "https://www.googleapis.com/auth/monitoring", 156 | ] 157 | metadata = { 158 | disable-legacy-endpoints = "true" 159 | } 160 | workload_metadata_config { 161 | mode = "GKE_METADATA" 162 | } 163 | labels = { 164 | nodegroup = "private" 165 | kubeip = "ignore" 166 | } 167 | resource_labels = { 168 | environment = "demo" 169 | kubeip = "ignore" 170 | public = "false" 171 | } 172 | } 173 | network_config { 174 | enable_private_nodes = true 175 | } 176 | } 177 | 178 | # Create static public IP addresses 179 | resource "google_compute_address" "static_ip" { 180 | provider = google-beta 181 | project = var.project_id 182 | count = 5 183 | name = "static-ip${var.ipv6_support ? "v6": "v4"}-${count.index}" 184 | ip_version = var.ipv6_support ? "IPV6" : "IPV4" 185 | ipv6_endpoint_type = "VM" 186 | address_type = "EXTERNAL" 187 | region = google_container_cluster.kubeip_cluster.location 188 | subnetwork = var.ipv6_support ? google_compute_subnetwork.kubeip_subnet.id : "" 189 | labels = { 190 | environment = "demo" 191 | kubeip = "reserved" 192 | } 193 | } 194 | 195 | data "google_client_config" "provider" {} 196 | 197 | provider "kubernetes" { 198 | host = "https://${google_container_cluster.kubeip_cluster.endpoint}" 199 | token = data.google_client_config.provider.access_token 200 | cluster_ca_certificate = base64decode( 201 | google_container_cluster.kubeip_cluster.master_auth[0].cluster_ca_certificate, 202 | ) 203 | } 204 | 205 | # Create Kubernetes service account in kube-system namespace 206 | resource "kubernetes_service_account" "kubeip_service_account" { 207 | metadata { 208 | name = "kubeip-service-account" 209 | namespace = "kube-system" 210 | annotations = { 211 | "iam.gke.io/gcp-service-account" = google_service_account.kubeip_service_account.email 212 | } 213 | } 214 | depends_on = [ 215 | google_service_account.kubeip_service_account, 216 | google_container_cluster.kubeip_cluster 217 | ] 218 | } 219 | 220 | # Create cluster role with get node permission 221 | resource "kubernetes_cluster_role" "kubeip_cluster_role" { 222 | metadata { 223 | name = "kubeip-cluster-role" 224 | } 225 | rule { 226 | api_groups = ["*"] 227 | resources = ["nodes"] 228 | verbs = ["get"] 229 | } 230 | rule { 231 | api_groups = ["coordination.k8s.io"] 232 | resources = ["leases"] 233 | verbs = ["create", "delete", "get"] 234 | } 235 | depends_on = [ 236 | kubernetes_service_account.kubeip_service_account, 237 | google_container_cluster.kubeip_cluster 238 | ] 239 | } 240 | 241 | # Bind cluster role to kubeip service account 242 | resource "kubernetes_cluster_role_binding" "kubeip_cluster_role_binding" { 243 | metadata { 244 | name = "kubeip-cluster-role-binding" 245 | } 246 | role_ref { 247 | api_group = "rbac.authorization.k8s.io" 248 | kind = "ClusterRole" 249 | name = kubernetes_cluster_role.kubeip_cluster_role.metadata[0].name 250 | } 251 | subject { 252 | kind = "ServiceAccount" 253 | name = kubernetes_service_account.kubeip_service_account.metadata[0].name 254 | namespace = kubernetes_service_account.kubeip_service_account.metadata[0].namespace 255 | } 256 | depends_on = [ 257 | kubernetes_service_account.kubeip_service_account, 258 | kubernetes_cluster_role.kubeip_cluster_role 259 | ] 260 | } 261 | 262 | # Deploy KubeIP DaemonSet 263 | resource "kubernetes_daemonset" "kubeip_daemonset" { 264 | metadata { 265 | name = "kubeip-agent" 266 | namespace = "kube-system" 267 | labels = { 268 | app = "kubeip" 269 | } 270 | } 271 | spec { 272 | selector { 273 | match_labels = { 274 | app = "kubeip" 275 | } 276 | } 277 | strategy { 278 | type = "RollingUpdate" 279 | rolling_update { 280 | max_unavailable = 1 281 | } 282 | } 283 | template { 284 | metadata { 285 | labels = { 286 | app = "kubeip" 287 | } 288 | } 289 | spec { 290 | service_account_name = "kubeip-service-account" 291 | termination_grace_period_seconds = 30 292 | priority_class_name = "system-node-critical" 293 | toleration { 294 | effect = "NoSchedule" 295 | operator = "Exists" 296 | } 297 | toleration { 298 | effect = "NoExecute" 299 | operator = "Exists" 300 | } 301 | container { 302 | name = "kubeip-agent" 303 | image = "doitintl/kubeip-agent:${var.kubeip_version}" 304 | image_pull_policy = "Always" 305 | env { 306 | name = "NODE_NAME" 307 | value_from { 308 | field_ref { 309 | field_path = "spec.nodeName" 310 | } 311 | } 312 | } 313 | env { 314 | name = "FILTER" 315 | value = "labels.kubeip=reserved;labels.environment=demo" 316 | } 317 | env { 318 | name = "LOG_LEVEL" 319 | value = "debug" 320 | } 321 | env { 322 | name = "LOG_JSON" 323 | value = "true" 324 | } 325 | env { 326 | name = "LEASE_DURATION" 327 | value = "20" 328 | } 329 | env { 330 | name = "LEASE_NAMESPACE" 331 | value_from { 332 | field_ref { 333 | field_path = "metadata.namespace" 334 | } 335 | } 336 | } 337 | resources { 338 | requests = { 339 | cpu = "10m" 340 | memory = "32Mi" 341 | } 342 | } 343 | } 344 | node_selector = { 345 | nodegroup = "public" 346 | kubeip = "use" 347 | } 348 | } 349 | } 350 | } 351 | depends_on = [ 352 | kubernetes_service_account.kubeip_service_account, 353 | google_container_cluster.kubeip_cluster 354 | ] 355 | } 356 | -------------------------------------------------------------------------------- /internal/address/aws.go: -------------------------------------------------------------------------------- 1 | package address 2 | 3 | import ( 4 | "context" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/aws/aws-sdk-go-v2/config" 9 | "github.com/aws/aws-sdk-go-v2/service/ec2" 10 | "github.com/aws/aws-sdk-go-v2/service/ec2/types" 11 | "github.com/doitintl/kubeip/internal/cloud" 12 | "github.com/pkg/errors" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | const ( 17 | shorthandFilterTokens = 2 18 | ) 19 | 20 | type awsAssigner struct { 21 | region string 22 | logger *logrus.Entry 23 | instanceGetter cloud.Ec2InstanceGetter 24 | eipLister cloud.EipLister 25 | eipAssigner cloud.EipAssigner 26 | } 27 | 28 | func NewAwsAssigner(ctx context.Context, logger *logrus.Entry, region string) (Assigner, error) { 29 | // initialize AWS client 30 | cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) 31 | if err != nil { 32 | return nil, errors.Wrap(err, "failed to load AWS config") 33 | } 34 | 35 | // create AWS client for EC2 service in the given region with default config and credentials 36 | client := ec2.NewFromConfig(cfg) 37 | 38 | // initialize AWS instance getter 39 | instanceGetter := cloud.NewEc2InstanceGetter(client) 40 | 41 | // initialize AWS elastic IP lister 42 | eipLister := cloud.NewEipLister(client) 43 | 44 | // initialize AWS elastic IP internalAssigner 45 | eipAssigner := cloud.NewEipAssigner(client) 46 | 47 | return &awsAssigner{ 48 | region: region, 49 | logger: logger, 50 | instanceGetter: instanceGetter, 51 | eipLister: eipLister, 52 | eipAssigner: eipAssigner, 53 | }, nil 54 | } 55 | 56 | // parseShorthandFilter parses shorthand filter string into filter name and values 57 | // shorthand filter format: Name=string,Values=string,string ... 58 | // https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/describe-addresses.html#options 59 | func parseShorthandFilter(filter string) (string, []string, error) { 60 | // split filter by the first "," 61 | exp := strings.SplitN(filter, ",", shorthandFilterTokens) 62 | if len(exp) != shorthandFilterTokens { 63 | return "", nil, errors.New("invalid filter format; supported format Name=string,Values=string,string,") 64 | } 65 | // get filter name 66 | name := strings.Split(exp[0], "=") 67 | if len(name) != 2 || name[0] != "Name" { 68 | return "", nil, errors.New("invalid filter Name") 69 | } 70 | // get filter values 71 | values := strings.Split(exp[1], "=") 72 | if len(values) != 2 || values[0] != "Values" { 73 | return "", nil, errors.New("invalid filter Values list") 74 | } 75 | listValues := strings.Split(values[1], ",") 76 | return name[1], listValues, nil 77 | } 78 | 79 | func sortAddressesByTag(addresses []types.Address, key string) { 80 | sort.Slice(addresses, func(i, j int) bool { 81 | if addresses[i].Tags == nil { 82 | return false 83 | } 84 | if addresses[j].Tags == nil { 85 | return true 86 | } 87 | for _, tag := range addresses[i].Tags { 88 | if *tag.Key == key { 89 | for _, tag2 := range addresses[j].Tags { 90 | if *tag2.Key == key { 91 | return *tag.Value < *tag2.Value 92 | } 93 | } 94 | } 95 | } 96 | return false 97 | }) 98 | } 99 | 100 | // sortAddressesByField sorts addresses by the given field 101 | // if sortBy is Tag:, sort addresses by tag value 102 | func sortAddressesByField(addresses []types.Address, sortBy string) { 103 | // if sortBy is Tag:, sort addresses by tag value 104 | if strings.HasPrefix(sortBy, "Tag:") { 105 | key := strings.TrimPrefix(sortBy, "Tag:") 106 | sortAddressesByTag(addresses, key) 107 | return // return if sortBy is Tag: 108 | } 109 | // sort addresses by orderBy field 110 | switch sortBy { 111 | case "AllocationId": 112 | sort.Slice(addresses, func(i, j int) bool { 113 | return *addresses[i].AllocationId < *addresses[j].AllocationId 114 | }) 115 | case "AssociationId": 116 | sort.Slice(addresses, func(i, j int) bool { 117 | return *addresses[i].AssociationId < *addresses[j].AssociationId 118 | }) 119 | case "Domain": 120 | sort.Slice(addresses, func(i, j int) bool { 121 | return addresses[i].Domain < addresses[j].Domain 122 | }) 123 | case "InstanceId": 124 | sort.Slice(addresses, func(i, j int) bool { 125 | return *addresses[i].InstanceId < *addresses[j].InstanceId 126 | }) 127 | case "NetworkInterfaceId": 128 | sort.Slice(addresses, func(i, j int) bool { 129 | return *addresses[i].NetworkInterfaceId < *addresses[j].NetworkInterfaceId 130 | }) 131 | case "NetworkInterfaceOwnerId": 132 | sort.Slice(addresses, func(i, j int) bool { 133 | return *addresses[i].NetworkInterfaceOwnerId < *addresses[j].NetworkInterfaceOwnerId 134 | }) 135 | case "PrivateIpAddress": 136 | sort.Slice(addresses, func(i, j int) bool { 137 | return *addresses[i].PrivateIpAddress < *addresses[j].PrivateIpAddress 138 | }) 139 | case "PublicIp": 140 | sort.Slice(addresses, func(i, j int) bool { 141 | return *addresses[i].PublicIp < *addresses[j].PublicIp 142 | }) 143 | case "PublicIpv4Pool": 144 | sort.Slice(addresses, func(i, j int) bool { 145 | return *addresses[i].PublicIpv4Pool < *addresses[j].PublicIpv4Pool 146 | }) 147 | } 148 | } 149 | 150 | func (a *awsAssigner) forceCheckAddressAssigned(ctx context.Context, allocationID string) (bool, error) { 151 | // get elastic IP attached to the allocation ID 152 | filters := make(map[string][]string) 153 | filters["allocation-id"] = []string{allocationID} 154 | addresses, err := a.eipLister.List(ctx, filters, true) 155 | if err != nil { 156 | return false, errors.Wrapf(err, "failed to list elastic IPs by allocation-id %s", allocationID) 157 | } 158 | if len(addresses) == 0 { 159 | return false, nil 160 | } 161 | // check if the first address (and the only) is assigned 162 | if addresses[0].AssociationId != nil { 163 | return true, nil 164 | } 165 | return false, nil 166 | } 167 | 168 | func (a *awsAssigner) Assign(ctx context.Context, instanceID, _ string, filter []string, orderBy string) (string, error) { 169 | // get elastic IP attached to the instance 170 | err := a.checkElasticIPAssigned(ctx, instanceID) 171 | if err != nil { 172 | return "", errors.Wrapf(err, "check if elastic IP is already assigned to instance %s", instanceID) 173 | } 174 | 175 | // get available elastic IPs based on filter and orderBy 176 | addresses, err := a.getAvailableElasticIPs(ctx, filter, orderBy) 177 | if err != nil { 178 | return "", errors.Wrap(err, "failed to get available elastic IPs") 179 | } 180 | 181 | // get EC2 instance 182 | instance, err := a.instanceGetter.Get(ctx, instanceID, a.region) 183 | if err != nil { 184 | return "", errors.Wrapf(err, "failed to get instance %s", instanceID) 185 | } 186 | // get primary network interface ID with public IP address (DeviceIndex == 0) 187 | networkInterfaceID, err := a.getNetworkInterfaceID(instance) 188 | if err != nil { 189 | return "", errors.Wrapf(err, "failed to get network interface ID for instance %s", instanceID) 190 | } 191 | 192 | // try to assign available addresses until succeeds 193 | // due to concurrency, it is possible that another kubeip instance will assign the same address 194 | var assignedAddress string 195 | for i := range addresses { 196 | a.logger.WithFields(logrus.Fields{ 197 | "instance": instanceID, 198 | "address": *addresses[i].PublicIp, 199 | "allocation_id": *addresses[i].AllocationId, 200 | "networkInterfaceID": networkInterfaceID, 201 | }).Debug("assigning elastic IP to the instance") 202 | err = a.tryAssignAddress(ctx, &addresses[i], networkInterfaceID, instanceID) 203 | if err != nil { 204 | a.logger.WithError(err).Warn("failed to assign elastic IP address") 205 | a.logger.Debug("retrying with another address") 206 | } else { 207 | a.logger.WithFields(logrus.Fields{ 208 | "instance": instanceID, 209 | "address": *addresses[i].PublicIp, 210 | "allocation_id": *addresses[i].AllocationId, 211 | }).Info("elastic IP assigned to the instance") 212 | assignedAddress = *addresses[i].PublicIp 213 | break // break if address assigned successfully 214 | } 215 | } 216 | if err != nil { 217 | return "", errors.Wrap(err, "failed to assign elastic IP address") 218 | } 219 | return assignedAddress, nil 220 | } 221 | 222 | func (a *awsAssigner) tryAssignAddress(ctx context.Context, address *types.Address, networkInterfaceID, instanceID string) error { 223 | // force check if address is already assigned (reduce the chance of assigning the same address by multiple kubeip instances) 224 | addressAssigned, err := a.forceCheckAddressAssigned(ctx, *address.AllocationId) 225 | if err != nil { 226 | return errors.Wrapf(err, "failed to check if address %s is assigned", *address.PublicIp) 227 | } 228 | if addressAssigned { 229 | return errors.Errorf("address %s is already assigned", *address.PublicIp) 230 | } 231 | if err = a.eipAssigner.Assign(ctx, networkInterfaceID, *address.AllocationId); err != nil { 232 | return errors.Wrapf(err, "failed to assign elastic IP %s to the instance %s", *address.PublicIp, instanceID) 233 | } 234 | return nil 235 | } 236 | 237 | func (a *awsAssigner) getNetworkInterfaceID(instance *types.Instance) (string, error) { 238 | // get network interface ID 239 | if len(instance.NetworkInterfaces) == 0 { 240 | return "", errors.Errorf("no network interfaces found for instance %s", *instance.InstanceId) 241 | } 242 | // get primary network interface ID with public IP address (DeviceIndex == 0) 243 | networkInterfaceID := "" 244 | for _, ni := range instance.NetworkInterfaces { 245 | if ni.Association != nil && ni.Association.PublicIp != nil && 246 | ni.Attachment != nil && ni.Attachment.DeviceIndex != nil && *ni.Attachment.DeviceIndex == 0 { 247 | networkInterfaceID = *ni.NetworkInterfaceId 248 | break 249 | } 250 | } 251 | if networkInterfaceID == "" { 252 | return "", errors.Errorf("no network interfaces with public IP address found for instance %s", *instance.InstanceId) 253 | } 254 | return networkInterfaceID, nil 255 | } 256 | 257 | func (a *awsAssigner) checkElasticIPAssigned(ctx context.Context, instanceID string) error { 258 | filters := make(map[string][]string) 259 | filters["instance-id"] = []string{instanceID} 260 | addresses, err := a.eipLister.List(ctx, filters, true) 261 | if err != nil { 262 | return errors.Wrapf(err, "failed to list elastic IPs attached to instance %s", instanceID) 263 | } 264 | if len(addresses) > 0 { 265 | return ErrStaticIPAlreadyAssigned 266 | } 267 | return nil 268 | } 269 | 270 | func (a *awsAssigner) getAssignedElasticIP(ctx context.Context, instanceID string) (*types.Address, error) { 271 | // get elastic IP attached to the instance 272 | filters := make(map[string][]string) 273 | filters["instance-id"] = []string{instanceID} 274 | addresses, err := a.eipLister.List(ctx, filters, true) 275 | if err != nil { 276 | return nil, errors.Wrapf(err, "failed to list elastic IPs attached to instance %s", instanceID) 277 | } 278 | if len(addresses) == 0 { 279 | return nil, ErrNoStaticIPAssigned 280 | } 281 | return &addresses[0], nil 282 | } 283 | 284 | func (a *awsAssigner) getAvailableElasticIPs(ctx context.Context, filter []string, orderBy string) ([]types.Address, error) { 285 | filters := make(map[string][]string) 286 | for _, f := range filter { 287 | name, values, err := parseShorthandFilter(f) 288 | if err != nil { 289 | return nil, errors.Wrapf(err, "failed to parse filter %s", f) 290 | } 291 | filters[name] = values 292 | } 293 | addresses, err := a.eipLister.List(ctx, filters, false) 294 | if err != nil { 295 | return nil, errors.Wrap(err, "failed to list available elastic IPs") 296 | } 297 | if len(addresses) == 0 { 298 | return nil, errors.Errorf("no available elastic IPs") 299 | } 300 | // sort addresses by orderBy field 301 | sortAddressesByField(addresses, orderBy) 302 | // log available addresses IPs 303 | ips := make([]string, 0, len(addresses)) 304 | for _, address := range addresses { 305 | ips = append(ips, *address.PublicIp) 306 | } 307 | a.logger.WithField("addresses", ips).Debugf("Found %d available addresses", len(addresses)) 308 | 309 | return addresses, nil 310 | } 311 | 312 | func (a *awsAssigner) Unassign(ctx context.Context, instanceID, _ string) error { 313 | // get elastic IP attached to the instance 314 | address, err := a.getAssignedElasticIP(ctx, instanceID) 315 | if err != nil { 316 | return errors.Wrapf(err, "check if elastic IP is assigned to instance %s", instanceID) 317 | } 318 | // unassign elastic IP from the instance 319 | if err = a.eipAssigner.Unassign(ctx, *address.AssociationId); err != nil { 320 | return errors.Wrap(err, "failed to unassign elastic IP") 321 | } 322 | a.logger.WithFields(logrus.Fields{ 323 | "instance": instanceID, 324 | "address": *address.PublicIp, 325 | "allocation_id": *address.AllocationId, 326 | "associationId": *address.AssociationId, 327 | }).Info("elastic IP unassigned from the instance") 328 | 329 | return nil 330 | } 331 | -------------------------------------------------------------------------------- /cmd/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/doitintl/kubeip/internal/address" 10 | "github.com/doitintl/kubeip/internal/config" 11 | "github.com/doitintl/kubeip/internal/node" 12 | "github.com/doitintl/kubeip/internal/types" 13 | mocks "github.com/doitintl/kubeip/mocks/address" 14 | nodeMocks "github.com/doitintl/kubeip/mocks/node" 15 | "github.com/pkg/errors" 16 | tmock "github.com/stretchr/testify/mock" 17 | "k8s.io/client-go/kubernetes/fake" 18 | ) 19 | 20 | func Test_assignAddress(t *testing.T) { 21 | type args struct { 22 | c context.Context 23 | assignerFn func(t *testing.T) address.Assigner 24 | node *types.Node 25 | cfg *config.Config 26 | } 27 | tests := []struct { 28 | name string 29 | args args 30 | address string 31 | wantErr bool 32 | }{ 33 | { 34 | name: "assign address successfully", 35 | address: "1.1.1.1", 36 | args: args{ 37 | c: context.Background(), 38 | assignerFn: func(t *testing.T) address.Assigner { 39 | mock := mocks.NewAssigner(t) 40 | mock.EXPECT().Assign(tmock.Anything, "test-instance", "test-zone", []string{"test-filter"}, "test-order-by").Return("1.1.1.1", nil) 41 | return mock 42 | }, 43 | node: &types.Node{ 44 | Name: "test-node", 45 | Instance: "test-instance", 46 | Region: "test-region", 47 | Zone: "test-zone", 48 | }, 49 | cfg: &config.Config{ 50 | Filter: []string{"test-filter"}, 51 | OrderBy: "test-order-by", 52 | RetryAttempts: 3, 53 | RetryInterval: time.Millisecond, 54 | LeaseDuration: 1, 55 | }, 56 | }, 57 | }, 58 | { 59 | name: "assign address after a few retries", 60 | address: "1.1.1.1", 61 | args: args{ 62 | c: context.Background(), 63 | assignerFn: func(t *testing.T) address.Assigner { 64 | mock := mocks.NewAssigner(t) 65 | mock.EXPECT().Assign(tmock.Anything, "test-instance", "test-zone", []string{"test-filter"}, "test-order-by").Return("", errors.New("first error")).Once() 66 | mock.EXPECT().Assign(tmock.Anything, "test-instance", "test-zone", []string{"test-filter"}, "test-order-by").Return("", errors.New("second error")).Once() 67 | mock.EXPECT().Assign(tmock.Anything, "test-instance", "test-zone", []string{"test-filter"}, "test-order-by").Return("1.1.1.1", nil).Once() 68 | return mock 69 | }, 70 | node: &types.Node{ 71 | Name: "test-node", 72 | Instance: "test-instance", 73 | Region: "test-region", 74 | Zone: "test-zone", 75 | }, 76 | cfg: &config.Config{ 77 | Filter: []string{"test-filter"}, 78 | OrderBy: "test-order-by", 79 | RetryAttempts: 3, 80 | RetryInterval: time.Millisecond, 81 | LeaseDuration: 1, 82 | }, 83 | }, 84 | }, 85 | { 86 | name: "error after a few retries and reached maximum number of retries", 87 | args: args{ 88 | c: context.Background(), 89 | assignerFn: func(t *testing.T) address.Assigner { 90 | mock := mocks.NewAssigner(t) 91 | mock.EXPECT().Assign(tmock.Anything, "test-instance", "test-zone", []string{"test-filter"}, "test-order-by").Return("", errors.New("error")).Times(4) 92 | return mock 93 | }, 94 | node: &types.Node{ 95 | Name: "test-node", 96 | Instance: "test-instance", 97 | Region: "test-region", 98 | Zone: "test-zone", 99 | }, 100 | cfg: &config.Config{ 101 | Filter: []string{"test-filter"}, 102 | OrderBy: "test-order-by", 103 | RetryAttempts: 3, 104 | RetryInterval: time.Millisecond, 105 | LeaseDuration: 1, 106 | }, 107 | }, 108 | wantErr: true, 109 | }, 110 | { 111 | name: "context cancelled while assigning addresses", 112 | args: args{ 113 | c: func() context.Context { 114 | ctx, cancel := context.WithCancel(context.Background()) 115 | go func() { 116 | // Simulate a shutdown signal being received after a short delay 117 | time.Sleep(20 * time.Millisecond) 118 | cancel() 119 | }() 120 | return ctx 121 | }(), 122 | assignerFn: func(t *testing.T) address.Assigner { 123 | mock := mocks.NewAssigner(t) 124 | mock.EXPECT().Assign(tmock.Anything, "test-instance", "test-zone", []string{"test-filter"}, "test-order-by").Return("", errors.New("error")).Maybe() 125 | return mock 126 | }, 127 | node: &types.Node{ 128 | Name: "test-node", 129 | Instance: "test-instance", 130 | Region: "test-region", 131 | Zone: "test-zone", 132 | }, 133 | cfg: &config.Config{ 134 | Filter: []string{"test-filter"}, 135 | OrderBy: "test-order-by", 136 | RetryAttempts: 10, 137 | RetryInterval: 5 * time.Millisecond, 138 | LeaseDuration: 1, 139 | }, 140 | }, 141 | wantErr: true, 142 | }, 143 | { 144 | name: "error after a few retries and context is done", 145 | args: args{ 146 | c: func() context.Context { 147 | ctx, _ := context.WithTimeout(context.Background(), 10*time.Millisecond) //nolint:govet 148 | return ctx 149 | }(), 150 | assignerFn: func(t *testing.T) address.Assigner { 151 | mock := mocks.NewAssigner(t) 152 | mock.EXPECT().Assign(tmock.Anything, "test-instance", "test-zone", []string{"test-filter"}, "test-order-by").Return("", errors.New("error")).Maybe() 153 | return mock 154 | }, 155 | node: &types.Node{ 156 | Name: "test-node", 157 | Instance: "test-instance", 158 | Region: "test-region", 159 | Zone: "test-zone", 160 | }, 161 | cfg: &config.Config{ 162 | Filter: []string{"test-filter"}, 163 | OrderBy: "test-order-by", 164 | RetryAttempts: 3, 165 | RetryInterval: 15 * time.Millisecond, 166 | LeaseDuration: 1, 167 | }, 168 | }, 169 | wantErr: true, 170 | }, 171 | } 172 | for _, tt := range tests { 173 | t.Run(tt.name, func(t *testing.T) { 174 | log := prepareLogger("debug", false) 175 | assigner := tt.args.assignerFn(t) 176 | client := fake.NewSimpleClientset() 177 | assignedAddress, err := assignAddress(tt.args.c, log, client, assigner, tt.args.node, tt.args.cfg) 178 | if err != nil != tt.wantErr { 179 | t.Errorf("assignAddress() error = %v, wantErr %v", err, tt.wantErr) 180 | } else if assignedAddress != tt.address { 181 | t.Fatalf("assignAddress() = %v, want %v", assignedAddress, tt.address) 182 | } 183 | }) 184 | } 185 | } 186 | 187 | func Test_waitForAddressToBeReported(t *testing.T) { 188 | type args struct { 189 | c context.Context 190 | explorerFn func(t *testing.T) node.Explorer 191 | node *types.Node 192 | address string 193 | cfg *config.Config 194 | } 195 | tests := []struct { 196 | name string 197 | args args 198 | wantErr bool 199 | }{ 200 | { 201 | name: "address reported with no retries", 202 | args: args{ 203 | c: context.Background(), 204 | address: "1.1.1.1", 205 | explorerFn: func(t *testing.T) node.Explorer { 206 | mock := nodeMocks.NewExplorer(t) 207 | mock.EXPECT().GetNode(tmock.Anything, "test-node").Return( 208 | &types.Node{ 209 | Name: "test-node", 210 | Instance: "test-instance", 211 | Region: "test-region", 212 | Zone: "test-zone", 213 | ExternalIPs: []net.IP{net.IPv4(1, 1, 1, 1)}, 214 | }, 215 | nil, 216 | ) 217 | return mock 218 | }, 219 | node: &types.Node{ 220 | Name: "test-node", 221 | Instance: "test-instance", 222 | Region: "test-region", 223 | Zone: "test-zone", 224 | }, 225 | cfg: &config.Config{ 226 | Filter: []string{"test-filter"}, 227 | OrderBy: "test-order-by", 228 | RetryAttempts: 3, 229 | RetryInterval: time.Millisecond, 230 | LeaseDuration: 1, 231 | }, 232 | }, 233 | }, 234 | { 235 | name: "address reported after a few retries", 236 | args: args{ 237 | c: context.Background(), 238 | address: "1.1.1.1", 239 | explorerFn: func(t *testing.T) node.Explorer { 240 | mock := nodeMocks.NewExplorer(t) 241 | mock.EXPECT().GetNode(tmock.Anything, "test-node").Return(&types.Node{ 242 | Name: "test-node", 243 | Instance: "test-instance", 244 | Region: "test-region", 245 | Zone: "test-zone", 246 | ExternalIPs: []net.IP{net.IPv4(9, 9, 9, 9)}, 247 | }, nil).Times(3) 248 | mock.EXPECT().GetNode(tmock.Anything, "test-node").Return(&types.Node{ 249 | Name: "test-node", 250 | Instance: "test-instance", 251 | Region: "test-region", 252 | Zone: "test-zone", 253 | ExternalIPs: []net.IP{net.IPv4(1, 1, 1, 1)}, 254 | }, nil).Once() 255 | return mock 256 | }, 257 | node: &types.Node{ 258 | Name: "test-node", 259 | Instance: "test-instance", 260 | Region: "test-region", 261 | Zone: "test-zone", 262 | }, 263 | cfg: &config.Config{ 264 | Filter: []string{"test-filter"}, 265 | OrderBy: "test-order-by", 266 | RetryAttempts: 3, 267 | RetryInterval: time.Millisecond, 268 | LeaseDuration: 1, 269 | }, 270 | }, 271 | }, 272 | { 273 | name: "error after a few retries and reached maximum number of retries", 274 | args: args{ 275 | c: context.Background(), 276 | explorerFn: func(t *testing.T) node.Explorer { 277 | mock := nodeMocks.NewExplorer(t) 278 | mock.EXPECT().GetNode(tmock.Anything, "test-node").Return(&types.Node{ 279 | Name: "test-node", 280 | Instance: "test-instance", 281 | Region: "test-region", 282 | Zone: "test-zone", 283 | ExternalIPs: []net.IP{net.IPv4(9, 9, 9, 9)}, 284 | }, nil).Times(4) 285 | mock.EXPECT().GetNode(tmock.Anything, "test-node").Return(&types.Node{ 286 | Name: "test-node", 287 | Instance: "test-instance", 288 | Region: "test-region", 289 | Zone: "test-zone", 290 | ExternalIPs: []net.IP{net.IPv4(1, 1, 1, 1)}, 291 | }, nil).Times(0) 292 | return mock 293 | }, 294 | node: &types.Node{ 295 | Name: "test-node", 296 | Instance: "test-instance", 297 | Region: "test-region", 298 | Zone: "test-zone", 299 | }, 300 | cfg: &config.Config{ 301 | Filter: []string{"test-filter"}, 302 | OrderBy: "test-order-by", 303 | RetryAttempts: 3, 304 | RetryInterval: time.Millisecond, 305 | LeaseDuration: 1, 306 | }, 307 | }, 308 | wantErr: true, 309 | }, 310 | { 311 | name: "context cancelled while waiting for address to be reported", 312 | args: args{ 313 | c: func() context.Context { 314 | ctx, cancel := context.WithCancel(context.Background()) 315 | go func() { 316 | // Simulate a shutdown signal being received after a short delay 317 | time.Sleep(20 * time.Millisecond) 318 | cancel() 319 | }() 320 | return ctx 321 | }(), 322 | explorerFn: func(t *testing.T) node.Explorer { 323 | mock := nodeMocks.NewExplorer(t) 324 | mock.EXPECT().GetNode(tmock.Anything, "test-node").Return(nil, errors.New("error")).Maybe() 325 | return mock 326 | }, 327 | node: &types.Node{ 328 | Name: "test-node", 329 | Instance: "test-instance", 330 | Region: "test-region", 331 | Zone: "test-zone", 332 | }, 333 | cfg: &config.Config{ 334 | Filter: []string{"test-filter"}, 335 | OrderBy: "test-order-by", 336 | RetryAttempts: 10, 337 | RetryInterval: 5 * time.Millisecond, 338 | LeaseDuration: 1, 339 | }, 340 | }, 341 | wantErr: true, 342 | }, 343 | { 344 | name: "error after a few retries and context is done", 345 | args: args{ 346 | c: func() context.Context { 347 | ctx, _ := context.WithTimeout(context.Background(), 10*time.Millisecond) //nolint:govet 348 | return ctx 349 | }(), 350 | explorerFn: func(t *testing.T) node.Explorer { 351 | mock := nodeMocks.NewExplorer(t) 352 | mock.EXPECT().GetNode(tmock.Anything, "test-node").Return(nil, errors.New("error")).Maybe() 353 | return mock 354 | }, 355 | node: &types.Node{ 356 | Name: "test-node", 357 | Instance: "test-instance", 358 | Region: "test-region", 359 | Zone: "test-zone", 360 | }, 361 | cfg: &config.Config{ 362 | Filter: []string{"test-filter"}, 363 | OrderBy: "test-order-by", 364 | RetryAttempts: 3, 365 | RetryInterval: 15 * time.Millisecond, 366 | LeaseDuration: 1, 367 | }, 368 | }, 369 | wantErr: true, 370 | }, 371 | } 372 | for _, tt := range tests { 373 | t.Run(tt.name, func(t *testing.T) { 374 | log := prepareLogger("debug", false) 375 | explorer := tt.args.explorerFn(t) 376 | err := waitForAddressToBeReported(tt.args.c, log, explorer, tt.args.node, tt.args.address, tt.args.cfg) 377 | if err != nil != tt.wantErr { 378 | t.Errorf("waitForAddressToBeReported() error = %v, wantErr %v", err, tt.wantErr) 379 | } 380 | }) 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /internal/address/oci.go: -------------------------------------------------------------------------------- 1 | package address 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/doitintl/kubeip/internal/cloud" 8 | "github.com/doitintl/kubeip/internal/config" 9 | "github.com/doitintl/kubeip/internal/types" 10 | "github.com/oracle/oci-go-sdk/v65/common" 11 | "github.com/oracle/oci-go-sdk/v65/core" 12 | "github.com/pkg/errors" 13 | "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // ociAssigner is an Assigner implementation for Oracle Cloud Infrastructure. 17 | type ociAssigner struct { 18 | logger *logrus.Entry 19 | filters *types.OCIFilters 20 | compartmentOCID string 21 | instanceSvc cloud.OCIInstanceService 22 | networkSvc cloud.OCINetworkService 23 | } 24 | 25 | // NewOCIAssigner creates a new Assigner for Oracle Cloud Infrastructure. 26 | func NewOCIAssigner(_ context.Context, logger *logrus.Entry, cfg *config.Config) (Assigner, error) { 27 | logger.WithFields( 28 | logrus.Fields{ 29 | "compartmentOCID": cfg.Project, 30 | "filters": cfg.Filter, 31 | }, 32 | ).Info("creating new OCI assigner with given config") 33 | 34 | // Parse the filters 35 | filters, err := parseOCIFilters(cfg) 36 | if err != nil { 37 | return nil, errors.Wrap(err, "failed to parse OCI filters") 38 | } 39 | if filters == nil { 40 | logger.Warn("no filters provided, any ip from the list of all public IPs present in the project can be used") 41 | } 42 | 43 | // Create a new instance svc 44 | computeSvc, err := cloud.NewOCIInstanceService() 45 | if err != nil { 46 | return nil, errors.Wrap(err, "failed to create compute service for OCI") 47 | } 48 | 49 | // Create a new network svc 50 | networkSvc, err := cloud.NewOCINetworkService() 51 | if err != nil { 52 | return nil, errors.Wrap(err, "failed to create network service for OCI") 53 | } 54 | 55 | return &ociAssigner{ 56 | logger: logger, 57 | filters: filters, 58 | instanceSvc: computeSvc, 59 | networkSvc: networkSvc, 60 | compartmentOCID: cfg.Project, 61 | }, nil 62 | } 63 | 64 | // Assign assigns reserved Public IP to the instance. 65 | // If the instance already has a public IP assigned, and it is from the reserved list, it returns the same IP. 66 | // Else it assigns a new public IP from the reserved list. 67 | func (a *ociAssigner) Assign(ctx context.Context, instanceOCID, _ string, _ []string, _ string) (string, error) { 68 | a.logger.WithField("instanceOCID", instanceOCID).Debug("starting process to assign reserved public IP to instance") 69 | 70 | // Get the primary VNIC 71 | vnic, err := a.getPrimaryVnicOfInstance(ctx, instanceOCID) 72 | if err != nil { 73 | return "", err 74 | } 75 | a.logger.WithField("primaryVnicOCID", *vnic.Id).Debugf("got primary VNIC of the instance %s", instanceOCID) 76 | 77 | // Handle already assigned public IP case 78 | alreadyAssigned, err := a.handlePublicIPAlreadyAssignedCase(ctx, vnic) 79 | if err != nil { 80 | return "", errors.Wrap(err, "failed to check if public ip is already assigned or not") 81 | } 82 | if alreadyAssigned { 83 | a.logger.WithField("alreadyAssignedIP", *vnic.PublicIp).Infof("reserved public IP already assigned on instance %s", instanceOCID) 84 | return *vnic.PublicIp, ErrStaticIPAlreadyAssigned 85 | } 86 | 87 | // Get primary VNIC private IP 88 | privateIP, err := a.networkSvc.GetPrimaryPrivateIPOfVnic(ctx, *vnic.Id) 89 | if err != nil { 90 | return "", errors.Wrap(err, "failed to get primary VNIC private IP") 91 | } 92 | a.logger.WithField("privateIPOCID", *privateIP.Id).Debugf("got primary VNIC private IP of the instance %s", instanceOCID) 93 | 94 | // Fetch all available reserved Public IPs that will be used for assignment 95 | reservedPublicIPList, err := a.fetchPublicIps(ctx, true, false) 96 | if err != nil { 97 | return "", errors.Wrap(err, "failed to get list of reserved public IPs") 98 | } 99 | if len(reservedPublicIPList) == 0 { 100 | return "", errors.New("no reserved public IPs available") 101 | } 102 | a.logger.WithField("reservedPublicIpList", reservedPublicIPList).Debug("got list of available reserved public IPs") 103 | 104 | // Try to assign an IP from the reserved public IP list 105 | for _, publicIP := range reservedPublicIPList { 106 | if err = a.tryAssignAddress(ctx, *privateIP.Id, *publicIP.Id); err == nil { 107 | a.logger.WithField("assignedIP", *publicIP.IpAddress).Infof("assigned IP %s to instance %s", *publicIP.IpAddress, instanceOCID) 108 | return *publicIP.IpAddress, nil 109 | } 110 | a.logger.Warnf("Failed to assign IP %s to instance %s: %v", *publicIP.IpAddress, instanceOCID, err) 111 | } 112 | 113 | return "", errors.New("failed to assign any IP") 114 | } 115 | 116 | // Unassign unassigns the public IP from the instance. 117 | // If assigned public IP is from the reserved public IP list, it unassigns the public IP. 118 | // Else it does nothing. 119 | func (a *ociAssigner) Unassign(ctx context.Context, instanceOCID, _ string) error { 120 | a.logger.WithField("instanceOCID", instanceOCID).Debug("starting process to unassign public IP from the instance") 121 | 122 | // Get the primary VNIC 123 | vnic, err := a.getPrimaryVnicOfInstance(ctx, instanceOCID) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | // If no public IP is assigned, return 129 | if vnic.PublicIp == nil { 130 | a.logger.Infof("no public ip assigned to the instance %s", instanceOCID) 131 | return ErrNoPublicIPAssigned 132 | } 133 | publicIP := vnic.PublicIp 134 | 135 | // Fetch assigned public IPs 136 | reservedPublicIPList, err := a.fetchPublicIps(ctx, true, true) 137 | if err != nil { 138 | return errors.Wrap(err, "failed to get list of reserved public IPs") 139 | } 140 | a.logger.WithField("reservedPublicIPList", reservedPublicIPList).Debug("got list of reserved public IPs") 141 | 142 | // Check if assigned public ip is from the reserved public IP list 143 | for _, ip := range reservedPublicIPList { 144 | if *ip.IpAddress == *publicIP && ip.LifecycleState == core.PublicIpLifecycleStateAssigned { 145 | // Unassign the public IP 146 | if err := a.networkSvc.UpdatePublicIP(ctx, *ip.Id, ""); err != nil { 147 | return errors.Wrap(err, "failed to unassign public IP assigned to private IP") 148 | } 149 | return nil 150 | } 151 | } 152 | 153 | return errors.New("public IP not assigned from reserved list") 154 | } 155 | 156 | // getPrimaryVnicOfInstance returns the primary VNIC of the instance from the VNIC attachment. 157 | func (a *ociAssigner) getPrimaryVnicOfInstance(ctx context.Context, instanceOCID string) (*core.Vnic, error) { 158 | // Get VNIC attachment of the instance 159 | vnicAttachment, err := a.instanceSvc.ListVnicAttachments(ctx, a.compartmentOCID, instanceOCID) 160 | if err != nil { 161 | return nil, errors.Wrap(err, "failed to list VNIC attachments") 162 | } 163 | 164 | // Get the primary VNIC 165 | vnic, err := a.networkSvc.GetPrimaryVnic(ctx, vnicAttachment) 166 | if err != nil { 167 | return nil, errors.Wrap(err, "failed to get primary VNIC") 168 | } 169 | 170 | return vnic, nil 171 | } 172 | 173 | // handlePublicIPAlreadyAssignedCase handles the case when the public IP is already assigned to the instance. 174 | // It returns true if the public IP is already assigned to the instance from the reserved IP list. In this case, do nothing. 175 | // 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. 176 | // Following are the cases and actions for each case: 177 | // - Case1: Public IP is already assigned to the instance from the reserved IP list: Do nothing 178 | // - Case2: Public IP is assigned to the instance but not from the reserved IP list: Unassign the public IP 179 | // - Case3: Public IP is assigned to the instance, but it is ephemeral: Delete the ephemeral public IP 180 | // - Case4: Unhandled case: Return error 181 | // 182 | //nolint:gocognit 183 | func (a *ociAssigner) handlePublicIPAlreadyAssignedCase(ctx context.Context, vnic *core.Vnic) (bool, error) { 184 | if vnic == nil { 185 | return false, nil 186 | } 187 | publicIP := vnic.PublicIp 188 | if publicIP != nil { 189 | // Case1 190 | // Fetch all reserved public IPs that are assigned to the private IPs 191 | list, err := a.fetchPublicIps(ctx, true, true) 192 | if err != nil { 193 | return false, errors.Wrap(err, "failed to list reserved public IPs assigned to private IP") 194 | } 195 | for _, ip := range list { 196 | if *ip.IpAddress == *publicIP { 197 | return true, nil 198 | } 199 | } 200 | 201 | // Case2 202 | // Fetch all public IPs that are assigned to the private IPs 203 | list, err = a.fetchPublicIps(ctx, false, true) 204 | if err != nil { 205 | return false, errors.Wrap(err, "failed to list public IPs assigned to private IP") 206 | } 207 | for _, ip := range list { 208 | if *ip.IpAddress == *publicIP { 209 | // Unassign the public IP 210 | if err = a.networkSvc.UpdatePublicIP(ctx, *ip.Id, ""); err != nil { 211 | return false, errors.Wrap(err, "failed to unassign public IP assigned to private IP") 212 | } 213 | return false, nil 214 | } 215 | } 216 | 217 | // Case3 218 | // Fetch ephemeral public IPs assigned to private IPs 219 | if vnic.AvailabilityDomain == nil { 220 | return false, errors.New("availability domain not found") 221 | } 222 | list, err = a.fetchEphemeralPublicIPs(ctx, *vnic.AvailabilityDomain) 223 | if err != nil { 224 | return false, errors.Wrap(err, "failed to list ephemeral public IPs assigned to private IP") 225 | } 226 | for _, ip := range list { 227 | if *ip.IpAddress == *publicIP { 228 | // Delete the ephemeral public IP 229 | if err := a.networkSvc.DeletePublicIP(ctx, *ip.Id); err != nil { 230 | return false, errors.Wrap(err, "failed to delete ephemeral public IP assigned to private IP") 231 | } 232 | return false, nil 233 | } 234 | } 235 | 236 | // Case4 237 | // Unhandled case 238 | return false, errors.New("unhandled case: public IP is assigned to the instance but not from the reserved IP list") 239 | } 240 | 241 | return false, nil 242 | } 243 | 244 | // fetchPublicIps returns the list of public IPs. 245 | // If useFilter is set to true, it applies the filters. 246 | // It returns only available public IPs if inUse is set to false. 247 | // It returns only assigned public IPs if inUse is set to true. 248 | func (a *ociAssigner) fetchPublicIps(ctx context.Context, useFilter, inUse bool) ([]core.PublicIp, error) { 249 | filters := a.filters 250 | // If useFilter is set to false, do not apply the filters 251 | if !useFilter { 252 | filters = nil 253 | } 254 | list, err := a.networkSvc.ListPublicIps(ctx, &core.ListPublicIpsRequest{ 255 | Scope: core.ListPublicIpsScopeRegion, 256 | CompartmentId: common.String(a.compartmentOCID), 257 | }, filters) 258 | if err != nil { 259 | return nil, errors.Wrap(err, "failed to list public IPs") 260 | } 261 | 262 | lifecycleState := core.PublicIpLifecycleStateAvailable 263 | // If inUse is set to true, only return assigned public IPs 264 | if inUse { 265 | lifecycleState = core.PublicIpLifecycleStateAssigned 266 | } 267 | 268 | // Return IPs that match the given lifecycleState. 269 | var updatedList []core.PublicIp 270 | for _, ip := range list { 271 | if ip.LifecycleState == lifecycleState { 272 | updatedList = append(updatedList, ip) 273 | } 274 | } 275 | return updatedList, nil 276 | } 277 | 278 | // fetchEphemeralPublicIPs returns the list of ephemeral public IPs assigned to the private IPs in the availability domain. 279 | func (a *ociAssigner) fetchEphemeralPublicIPs(ctx context.Context, availabilityDomain string) ([]core.PublicIp, error) { 280 | list, err := a.networkSvc.ListPublicIps(ctx, &core.ListPublicIpsRequest{ 281 | Scope: core.ListPublicIpsScopeAvailabilityDomain, 282 | AvailabilityDomain: common.String(availabilityDomain), 283 | CompartmentId: common.String(a.compartmentOCID), 284 | }, nil) 285 | if err != nil { 286 | return nil, errors.Wrap(err, "failed to list ephemeral public IPs") 287 | } 288 | 289 | return list, nil 290 | } 291 | 292 | // tryAssignAddress tries to assign the public IP to the private IP. 293 | // If the public IP is not available, it returns an error. 294 | func (a *ociAssigner) tryAssignAddress(ctx context.Context, privateIPOCID, publicIPOCID string) error { 295 | // Fetch public IP details to check if it is available 296 | publicIP, err := a.networkSvc.GetPublicIP(ctx, publicIPOCID) 297 | if err != nil { 298 | return errors.Wrap(err, "failed to get public IP details") 299 | } 300 | if publicIP == nil { 301 | return errors.New("public IP not found") 302 | } 303 | 304 | // If public IP is not available, return 305 | if publicIP.LifecycleState != core.PublicIpLifecycleStateAvailable { 306 | return errors.New("public IP is not available") 307 | } 308 | 309 | // Assign the public IP to the private IP 310 | if err := a.networkSvc.UpdatePublicIP(ctx, *publicIP.Id, privateIPOCID); err != nil { 311 | return errors.Wrap(err, "failed to assign public IP") 312 | } 313 | 314 | return nil 315 | } 316 | 317 | // ParseOCIFilters parses the filters for OCI from the config. 318 | // All filters of freeformTags are combined with AND condition. 319 | // All filters of definedTags are combined with AND condition. 320 | // Filter should be in following format: 321 | // - "freeformTags.key1=value1" 322 | // - "definedTags.Namespace.key1=value1" 323 | func parseOCIFilters(cfg *config.Config) (*types.OCIFilters, error) { 324 | if cfg == nil { 325 | return nil, errors.New("config is nil") 326 | } 327 | 328 | freeformTags := make(map[string]string) 329 | 330 | for _, filter := range cfg.Filter { 331 | if strings.HasPrefix(filter, "freeformTags.") { 332 | key, value, err := types.ParseFreeformTagFilter(filter) 333 | if err != nil { 334 | return nil, errors.Wrap(err, "failed to parse freeform tag filter") 335 | } 336 | freeformTags[key] = value 337 | } else { 338 | return nil, errors.New("invalid filter format for OCI, should be in format freeformTags.key=value or definedTags.Namespace.key=value, found: " + filter) 339 | } 340 | } 341 | 342 | return &types.OCIFilters{ 343 | FreeformTags: freeformTags, 344 | }, nil 345 | } 346 | -------------------------------------------------------------------------------- /mocks/cloud/OCINetworkService.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.30.16. DO NOT EDIT. 2 | 3 | package mocks 4 | 5 | import ( 6 | context "context" 7 | 8 | core "github.com/oracle/oci-go-sdk/v65/core" 9 | mock "github.com/stretchr/testify/mock" 10 | 11 | types "github.com/doitintl/kubeip/internal/types" 12 | ) 13 | 14 | // OCINetworkService is an autogenerated mock type for the OCINetworkService type 15 | type OCINetworkService struct { 16 | mock.Mock 17 | } 18 | 19 | type OCINetworkService_Expecter struct { 20 | mock *mock.Mock 21 | } 22 | 23 | func (_m *OCINetworkService) EXPECT() *OCINetworkService_Expecter { 24 | return &OCINetworkService_Expecter{mock: &_m.Mock} 25 | } 26 | 27 | // DeletePublicIP provides a mock function with given fields: ctx, publicIPOCID 28 | func (_m *OCINetworkService) DeletePublicIP(ctx context.Context, publicIPOCID string) error { 29 | ret := _m.Called(ctx, publicIPOCID) 30 | 31 | if len(ret) == 0 { 32 | panic("no return value specified for DeletePublicIP") 33 | } 34 | 35 | var r0 error 36 | if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { 37 | r0 = rf(ctx, publicIPOCID) 38 | } else { 39 | r0 = ret.Error(0) 40 | } 41 | 42 | return r0 43 | } 44 | 45 | // OCINetworkService_DeletePublicIP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeletePublicIP' 46 | type OCINetworkService_DeletePublicIP_Call struct { 47 | *mock.Call 48 | } 49 | 50 | // DeletePublicIP is a helper method to define mock.On call 51 | // - ctx context.Context 52 | // - publicIPOCID string 53 | func (_e *OCINetworkService_Expecter) DeletePublicIP(ctx interface{}, publicIPOCID interface{}) *OCINetworkService_DeletePublicIP_Call { 54 | return &OCINetworkService_DeletePublicIP_Call{Call: _e.mock.On("DeletePublicIP", ctx, publicIPOCID)} 55 | } 56 | 57 | func (_c *OCINetworkService_DeletePublicIP_Call) Run(run func(ctx context.Context, publicIPOCID string)) *OCINetworkService_DeletePublicIP_Call { 58 | _c.Call.Run(func(args mock.Arguments) { 59 | run(args[0].(context.Context), args[1].(string)) 60 | }) 61 | return _c 62 | } 63 | 64 | func (_c *OCINetworkService_DeletePublicIP_Call) Return(_a0 error) *OCINetworkService_DeletePublicIP_Call { 65 | _c.Call.Return(_a0) 66 | return _c 67 | } 68 | 69 | func (_c *OCINetworkService_DeletePublicIP_Call) RunAndReturn(run func(context.Context, string) error) *OCINetworkService_DeletePublicIP_Call { 70 | _c.Call.Return(run) 71 | return _c 72 | } 73 | 74 | // GetPrimaryPrivateIPOfVnic provides a mock function with given fields: ctx, vnicOCID 75 | func (_m *OCINetworkService) GetPrimaryPrivateIPOfVnic(ctx context.Context, vnicOCID string) (*core.PrivateIp, error) { 76 | ret := _m.Called(ctx, vnicOCID) 77 | 78 | if len(ret) == 0 { 79 | panic("no return value specified for GetPrimaryPrivateIPOfVnic") 80 | } 81 | 82 | var r0 *core.PrivateIp 83 | var r1 error 84 | if rf, ok := ret.Get(0).(func(context.Context, string) (*core.PrivateIp, error)); ok { 85 | return rf(ctx, vnicOCID) 86 | } 87 | if rf, ok := ret.Get(0).(func(context.Context, string) *core.PrivateIp); ok { 88 | r0 = rf(ctx, vnicOCID) 89 | } else { 90 | if ret.Get(0) != nil { 91 | r0 = ret.Get(0).(*core.PrivateIp) 92 | } 93 | } 94 | 95 | if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { 96 | r1 = rf(ctx, vnicOCID) 97 | } else { 98 | r1 = ret.Error(1) 99 | } 100 | 101 | return r0, r1 102 | } 103 | 104 | // OCINetworkService_GetPrimaryPrivateIPOfVnic_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPrimaryPrivateIPOfVnic' 105 | type OCINetworkService_GetPrimaryPrivateIPOfVnic_Call struct { 106 | *mock.Call 107 | } 108 | 109 | // GetPrimaryPrivateIPOfVnic is a helper method to define mock.On call 110 | // - ctx context.Context 111 | // - vnicOCID string 112 | func (_e *OCINetworkService_Expecter) GetPrimaryPrivateIPOfVnic(ctx interface{}, vnicOCID interface{}) *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call { 113 | return &OCINetworkService_GetPrimaryPrivateIPOfVnic_Call{Call: _e.mock.On("GetPrimaryPrivateIPOfVnic", ctx, vnicOCID)} 114 | } 115 | 116 | func (_c *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call) Run(run func(ctx context.Context, vnicOCID string)) *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call { 117 | _c.Call.Run(func(args mock.Arguments) { 118 | run(args[0].(context.Context), args[1].(string)) 119 | }) 120 | return _c 121 | } 122 | 123 | func (_c *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call) Return(_a0 *core.PrivateIp, _a1 error) *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call { 124 | _c.Call.Return(_a0, _a1) 125 | return _c 126 | } 127 | 128 | func (_c *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call) RunAndReturn(run func(context.Context, string) (*core.PrivateIp, error)) *OCINetworkService_GetPrimaryPrivateIPOfVnic_Call { 129 | _c.Call.Return(run) 130 | return _c 131 | } 132 | 133 | // GetPrimaryVnic provides a mock function with given fields: ctx, vnicAttachments 134 | func (_m *OCINetworkService) GetPrimaryVnic(ctx context.Context, vnicAttachments []core.VnicAttachment) (*core.Vnic, error) { 135 | ret := _m.Called(ctx, vnicAttachments) 136 | 137 | if len(ret) == 0 { 138 | panic("no return value specified for GetPrimaryVnic") 139 | } 140 | 141 | var r0 *core.Vnic 142 | var r1 error 143 | if rf, ok := ret.Get(0).(func(context.Context, []core.VnicAttachment) (*core.Vnic, error)); ok { 144 | return rf(ctx, vnicAttachments) 145 | } 146 | if rf, ok := ret.Get(0).(func(context.Context, []core.VnicAttachment) *core.Vnic); ok { 147 | r0 = rf(ctx, vnicAttachments) 148 | } else { 149 | if ret.Get(0) != nil { 150 | r0 = ret.Get(0).(*core.Vnic) 151 | } 152 | } 153 | 154 | if rf, ok := ret.Get(1).(func(context.Context, []core.VnicAttachment) error); ok { 155 | r1 = rf(ctx, vnicAttachments) 156 | } else { 157 | r1 = ret.Error(1) 158 | } 159 | 160 | return r0, r1 161 | } 162 | 163 | // OCINetworkService_GetPrimaryVnic_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPrimaryVnic' 164 | type OCINetworkService_GetPrimaryVnic_Call struct { 165 | *mock.Call 166 | } 167 | 168 | // GetPrimaryVnic is a helper method to define mock.On call 169 | // - ctx context.Context 170 | // - vnicAttachments []core.VnicAttachment 171 | func (_e *OCINetworkService_Expecter) GetPrimaryVnic(ctx interface{}, vnicAttachments interface{}) *OCINetworkService_GetPrimaryVnic_Call { 172 | return &OCINetworkService_GetPrimaryVnic_Call{Call: _e.mock.On("GetPrimaryVnic", ctx, vnicAttachments)} 173 | } 174 | 175 | func (_c *OCINetworkService_GetPrimaryVnic_Call) Run(run func(ctx context.Context, vnicAttachments []core.VnicAttachment)) *OCINetworkService_GetPrimaryVnic_Call { 176 | _c.Call.Run(func(args mock.Arguments) { 177 | run(args[0].(context.Context), args[1].([]core.VnicAttachment)) 178 | }) 179 | return _c 180 | } 181 | 182 | func (_c *OCINetworkService_GetPrimaryVnic_Call) Return(_a0 *core.Vnic, _a1 error) *OCINetworkService_GetPrimaryVnic_Call { 183 | _c.Call.Return(_a0, _a1) 184 | return _c 185 | } 186 | 187 | func (_c *OCINetworkService_GetPrimaryVnic_Call) RunAndReturn(run func(context.Context, []core.VnicAttachment) (*core.Vnic, error)) *OCINetworkService_GetPrimaryVnic_Call { 188 | _c.Call.Return(run) 189 | return _c 190 | } 191 | 192 | // GetPublicIP provides a mock function with given fields: ctx, publicIPOCID 193 | func (_m *OCINetworkService) GetPublicIP(ctx context.Context, publicIPOCID string) (*core.PublicIp, error) { 194 | ret := _m.Called(ctx, publicIPOCID) 195 | 196 | if len(ret) == 0 { 197 | panic("no return value specified for GetPublicIP") 198 | } 199 | 200 | var r0 *core.PublicIp 201 | var r1 error 202 | if rf, ok := ret.Get(0).(func(context.Context, string) (*core.PublicIp, error)); ok { 203 | return rf(ctx, publicIPOCID) 204 | } 205 | if rf, ok := ret.Get(0).(func(context.Context, string) *core.PublicIp); ok { 206 | r0 = rf(ctx, publicIPOCID) 207 | } else { 208 | if ret.Get(0) != nil { 209 | r0 = ret.Get(0).(*core.PublicIp) 210 | } 211 | } 212 | 213 | if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { 214 | r1 = rf(ctx, publicIPOCID) 215 | } else { 216 | r1 = ret.Error(1) 217 | } 218 | 219 | return r0, r1 220 | } 221 | 222 | // OCINetworkService_GetPublicIP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetPublicIP' 223 | type OCINetworkService_GetPublicIP_Call struct { 224 | *mock.Call 225 | } 226 | 227 | // GetPublicIP is a helper method to define mock.On call 228 | // - ctx context.Context 229 | // - publicIPOCID string 230 | func (_e *OCINetworkService_Expecter) GetPublicIP(ctx interface{}, publicIPOCID interface{}) *OCINetworkService_GetPublicIP_Call { 231 | return &OCINetworkService_GetPublicIP_Call{Call: _e.mock.On("GetPublicIP", ctx, publicIPOCID)} 232 | } 233 | 234 | func (_c *OCINetworkService_GetPublicIP_Call) Run(run func(ctx context.Context, publicIPOCID string)) *OCINetworkService_GetPublicIP_Call { 235 | _c.Call.Run(func(args mock.Arguments) { 236 | run(args[0].(context.Context), args[1].(string)) 237 | }) 238 | return _c 239 | } 240 | 241 | func (_c *OCINetworkService_GetPublicIP_Call) Return(_a0 *core.PublicIp, _a1 error) *OCINetworkService_GetPublicIP_Call { 242 | _c.Call.Return(_a0, _a1) 243 | return _c 244 | } 245 | 246 | func (_c *OCINetworkService_GetPublicIP_Call) RunAndReturn(run func(context.Context, string) (*core.PublicIp, error)) *OCINetworkService_GetPublicIP_Call { 247 | _c.Call.Return(run) 248 | return _c 249 | } 250 | 251 | // ListPublicIps provides a mock function with given fields: ctx, request, filters 252 | func (_m *OCINetworkService) ListPublicIps(ctx context.Context, request *core.ListPublicIpsRequest, filters *types.OCIFilters) ([]core.PublicIp, error) { 253 | ret := _m.Called(ctx, request, filters) 254 | 255 | if len(ret) == 0 { 256 | panic("no return value specified for ListPublicIps") 257 | } 258 | 259 | var r0 []core.PublicIp 260 | var r1 error 261 | if rf, ok := ret.Get(0).(func(context.Context, *core.ListPublicIpsRequest, *types.OCIFilters) ([]core.PublicIp, error)); ok { 262 | return rf(ctx, request, filters) 263 | } 264 | if rf, ok := ret.Get(0).(func(context.Context, *core.ListPublicIpsRequest, *types.OCIFilters) []core.PublicIp); ok { 265 | r0 = rf(ctx, request, filters) 266 | } else { 267 | if ret.Get(0) != nil { 268 | r0 = ret.Get(0).([]core.PublicIp) 269 | } 270 | } 271 | 272 | if rf, ok := ret.Get(1).(func(context.Context, *core.ListPublicIpsRequest, *types.OCIFilters) error); ok { 273 | r1 = rf(ctx, request, filters) 274 | } else { 275 | r1 = ret.Error(1) 276 | } 277 | 278 | return r0, r1 279 | } 280 | 281 | // OCINetworkService_ListPublicIps_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListPublicIps' 282 | type OCINetworkService_ListPublicIps_Call struct { 283 | *mock.Call 284 | } 285 | 286 | // ListPublicIps is a helper method to define mock.On call 287 | // - ctx context.Context 288 | // - request *core.ListPublicIpsRequest 289 | // - filters *types.OCIFilters 290 | func (_e *OCINetworkService_Expecter) ListPublicIps(ctx interface{}, request interface{}, filters interface{}) *OCINetworkService_ListPublicIps_Call { 291 | return &OCINetworkService_ListPublicIps_Call{Call: _e.mock.On("ListPublicIps", ctx, request, filters)} 292 | } 293 | 294 | func (_c *OCINetworkService_ListPublicIps_Call) Run(run func(ctx context.Context, request *core.ListPublicIpsRequest, filters *types.OCIFilters)) *OCINetworkService_ListPublicIps_Call { 295 | _c.Call.Run(func(args mock.Arguments) { 296 | run(args[0].(context.Context), args[1].(*core.ListPublicIpsRequest), args[2].(*types.OCIFilters)) 297 | }) 298 | return _c 299 | } 300 | 301 | func (_c *OCINetworkService_ListPublicIps_Call) Return(_a0 []core.PublicIp, _a1 error) *OCINetworkService_ListPublicIps_Call { 302 | _c.Call.Return(_a0, _a1) 303 | return _c 304 | } 305 | 306 | func (_c *OCINetworkService_ListPublicIps_Call) RunAndReturn(run func(context.Context, *core.ListPublicIpsRequest, *types.OCIFilters) ([]core.PublicIp, error)) *OCINetworkService_ListPublicIps_Call { 307 | _c.Call.Return(run) 308 | return _c 309 | } 310 | 311 | // UpdatePublicIP provides a mock function with given fields: ctx, publicIPOCID, privateIPOCID 312 | func (_m *OCINetworkService) UpdatePublicIP(ctx context.Context, publicIPOCID string, privateIPOCID string) error { 313 | ret := _m.Called(ctx, publicIPOCID, privateIPOCID) 314 | 315 | if len(ret) == 0 { 316 | panic("no return value specified for UpdatePublicIP") 317 | } 318 | 319 | var r0 error 320 | if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { 321 | r0 = rf(ctx, publicIPOCID, privateIPOCID) 322 | } else { 323 | r0 = ret.Error(0) 324 | } 325 | 326 | return r0 327 | } 328 | 329 | // OCINetworkService_UpdatePublicIP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdatePublicIP' 330 | type OCINetworkService_UpdatePublicIP_Call struct { 331 | *mock.Call 332 | } 333 | 334 | // UpdatePublicIP is a helper method to define mock.On call 335 | // - ctx context.Context 336 | // - publicIPOCID string 337 | // - privateIPOCID string 338 | func (_e *OCINetworkService_Expecter) UpdatePublicIP(ctx interface{}, publicIPOCID interface{}, privateIPOCID interface{}) *OCINetworkService_UpdatePublicIP_Call { 339 | return &OCINetworkService_UpdatePublicIP_Call{Call: _e.mock.On("UpdatePublicIP", ctx, publicIPOCID, privateIPOCID)} 340 | } 341 | 342 | func (_c *OCINetworkService_UpdatePublicIP_Call) Run(run func(ctx context.Context, publicIPOCID string, privateIPOCID string)) *OCINetworkService_UpdatePublicIP_Call { 343 | _c.Call.Run(func(args mock.Arguments) { 344 | run(args[0].(context.Context), args[1].(string), args[2].(string)) 345 | }) 346 | return _c 347 | } 348 | 349 | func (_c *OCINetworkService_UpdatePublicIP_Call) Return(_a0 error) *OCINetworkService_UpdatePublicIP_Call { 350 | _c.Call.Return(_a0) 351 | return _c 352 | } 353 | 354 | func (_c *OCINetworkService_UpdatePublicIP_Call) RunAndReturn(run func(context.Context, string, string) error) *OCINetworkService_UpdatePublicIP_Call { 355 | _c.Call.Return(run) 356 | return _c 357 | } 358 | 359 | // 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. 360 | // The first argument is typically a *testing.T value. 361 | func NewOCINetworkService(t interface { 362 | mock.TestingT 363 | Cleanup(func()) 364 | }) *OCINetworkService { 365 | mock := &OCINetworkService{} 366 | mock.Mock.Test(t) 367 | 368 | t.Cleanup(func() { mock.AssertExpectations(t) }) 369 | 370 | return mock 371 | } 372 | --------------------------------------------------------------------------------