{"id":28578185,"url":"https://github.com/digitalocean/flipop","last_synced_at":"2025-08-28T22:27:20.771Z","repository":{"id":43838578,"uuid":"245918925","full_name":"digitalocean/flipop","owner":"digitalocean","description":"Floating IP Controller for Kubernetes","archived":false,"fork":false,"pushed_at":"2025-08-15T18:12:54.000Z","size":469,"stargazers_count":33,"open_issues_count":3,"forks_count":7,"subscribers_count":90,"default_branch":"main","last_synced_at":"2025-08-15T20:34:41.428Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/digitalocean.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2020-03-09T01:35:17.000Z","updated_at":"2025-08-15T18:12:59.000Z","dependencies_parsed_at":"2025-03-28T16:28:24.916Z","dependency_job_id":"201745a9-36e2-4de7-8ea4-dd22097613bd","html_url":"https://github.com/digitalocean/flipop","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/digitalocean/flipop","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digitalocean%2Fflipop","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digitalocean%2Fflipop/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digitalocean%2Fflipop/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digitalocean%2Fflipop/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/digitalocean","download_url":"https://codeload.github.com/digitalocean/flipop/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/digitalocean%2Fflipop/sbom","scorecard":{"id":342589,"data":{"date":"2025-08-11","repo":{"name":"github.com/digitalocean/flipop","commit":"8c2ae5a05f76ffb416bd9c5006933a0e9f742d5e"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":5,"checks":[{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Code-Review","score":6,"reason":"Found 13/21 approved changesets -- score normalized to 6","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/main.yml:1","Warn: no topLevel permission defined: .github/workflows/pr-build.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE.txt:0","Info: FSF or OSI recognized license: Apache License 2.0: LICENSE.txt:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/main.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/digitalocean/flipop/main.yml/main?enable=pin","Warn: third-party GitHubAction not pinned by hash: .github/workflows/main.yml:16: update your workflow using https://app.stepsecurity.io/secureworkflow/digitalocean/flipop/main.yml/main?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/pr-build.yml:13: update your workflow using https://app.stepsecurity.io/secureworkflow/digitalocean/flipop/pr-build.yml/main?enable=pin","Warn: containerImage not pinned by hash: Dockerfile:1","Warn: containerImage not pinned by hash: Dockerfile:7: pin your Docker image by updating debian:bookworm-slim to debian:bookworm-slim@sha256:b1a741487078b369e78119849663d7f1a5341ef2768798f7b7406c4240f86aef","Warn: containerImage not pinned by hash: Dockerfile.dev:3","Info:   0 out of   2 GitHub-owned GitHubAction dependencies pinned","Info:   0 out of   1 third-party GitHubAction dependencies pinned","Info:   0 out of   3 containerImage dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: github.com/digitalocean/.github/SECURITY.md:1","Info: Found linked content: github.com/digitalocean/.github/SECURITY.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: github.com/digitalocean/.github/SECURITY.md:1","Info: Found text in security policy: github.com/digitalocean/.github/SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Vulnerabilities","score":9,"reason":"1 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GO-2025-3595 / GHSA-vvgc-356p-c3xw"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 27 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-18T06:14:34.349Z","repository_id":43838578,"created_at":"2025-08-18T06:14:34.349Z","updated_at":"2025-08-18T06:14:34.349Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272568252,"owners_count":24956982,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-28T02:00:10.768Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-06-11T01:09:06.320Z","updated_at":"2025-08-28T22:27:20.765Z","avatar_url":"https://github.com/digitalocean.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Floating IP Operator (FLIPOP)\n\nFLIPOP is a Kubernetes operator that manages cloud-native Floating IPs (also referred to as Reserved IPs), BYOIPs (Bring Your Own IPs) and DNS records for targeted nodes and pods. It provides advanced traffic steering for workloads—especially latency-sensitive or UDP traffic—where built-in Kubernetes LoadBalancer services may not suffice.\n\nFLIPOP can be used to assign BYOIPs to K8s nodes by explicitly mentioning the list of BYOIPs in the `ips` list. If we mention just `desired_ips`, FLIPOP will try to assign the Floating IPs.\n---\n\n## Features\n\n* Assign and unassign Floating IPs and BYOIPs to Kubernetes nodes based on pod and node selectors.\n* Manage DNS A records containing floating or node IPs.\n* Support for multiple DNS providers (e.g., DigitalOcean, Cloudflare).\n* Expose rich Prometheus metrics for observability.\n* Graceful reconciliation loops with configurable retry/backoff.\n* Leader election for high-availability.\n\n---\n\n## Architecture\n\n1. **CRD Watchers**: Informers monitor `FloatingIPPool` and `NodeDNSRecordSet` resources.\n2. **Match Controller** (`nodematch`): Evaluates pods and nodes against label/taint-based criteria.\n3. **IP Controller** (`ip_controller`): Reconciles Floating IP assignments and updates status \u0026 annotations.\n4. **DNS Enabler/Disabler** (`nodedns`): Updates DNS records for matching nodes.\n5. **Metrics Collector** (`metrics`): Implements Prometheus `Collector` interfaces for each controller.\n6. **Leader Election** (`leaderelection`): Ensures only one active control loop per cluster.\n\n---\n\n## Custom Resources\n\n### FloatingIPPool\n\nManage Floating IPs and optional DNS records for pods matching specified criteria.\n\n```yaml\napiVersion: flipop.digitalocean.com/v1alpha1\nkind: FloatingIPPool\nmetadata:\n  name: ingress-pool\nspec:\n  provider: digitalocean          # IP provider\n  region: nyc3                   # Cloud region\n  desiredIPs: 3                  # Total IPs to allocate\n  assignmentCoolOffSeconds: 20   # Seconds to wait between ip assignments, defaults to 0 if not set\n  ips:                           # Static IP list (optional)\n    - 192.168.1.1\n    - 192.168.2.1\n  dnsRecordSet:                  # Optional DNS configuration (defaults to digitalocean)\n    recordName: hello\n    zone:      example.com\n    ttl:       30\n    provider:  digitalocean\n  match:                         # Node/pod matching criteria\n    podNamespace: ingress\n    podLabel:     app=nginx,component=controller\n    nodeLabel:    doks.digitalocean.com/node-pool=work\n    tolerations:\n      - key:    node.kubernetes.io/unschedulable\n        effect: NoSchedule\n```\n\n**Behavior**:\n\n* Allocates a number of Floating IPs equal to `desiredIPs`.\n  * By default, new floating IPs will be created\n  * If you wish to use existing Floating IPs or BYOIPs specify them in the list of `ips`\n* Assigns IPs to matching nodes (see Matching section below)\n* Updates DNS A record (if configured) using FloatingIPPool’s reserved IPs by default.\n  * Note this behavior is slightly different than how `NodeDNSRecordSet` works. `dnsRecordSet` will always update the DNS record with the nodes Floating IP address, where `NodeDNSRecordSet` must be configured to use the Floating IP address.\n* The annotation `flipop.digitalocean.com/ipv4-reserved-ip` is added to each node with the assigned Floating IP address as the value.\n\n---\n\n### NodeDNSRecordSet\n\nManage DNS A records for nodes matching specified criteria.\n\n```yaml\napiVersion: flipop.digitalocean.com/v1alpha1\nkind: NodeDNSRecordSet\nmetadata:\n  name: ingress-nodes\nspec:\n  provider: digitalocean         # DNS provider (defaults to digitalocean)\n  dnsRecordSet:\n    recordName: nodes.example.com\n    zone:      example.com\n    ttl:       120\n  addressType: flipop.digitalocean.com/ipv4-reserved-ip  # Use the node’s reserved IPv4 address (via annotation)\n  match:\n    nodeLabel:  doks.digitalocean.com/node-pool=work\n    podNamespace: ingress\n    podLabel:     app=nginx\n    tolerations:\n      - key:    node.kubernetes.io/unschedulable\n        effect: NoSchedule\n```\n\n**Field**:\n\n* `addressType`: Specifies which node address to publish in DNS. Options:\n    * `ExternalIP` (default): Uses each node’s external/public IP.\n    * `flipop.digitalocean.com/ipv4-reserved-ip`: Uses the node’s reserved IPv4 address assigned by a FloatingIPPool. Must be set explicitly when DNS should point to reserved IPs. When this addressType is specified that controller will look for the value of this annotation on each node to determine the reserved IP for the node.\n  * `InternalIP`: Uses the node’s internal Kubernetes cluster IP.\n\n**Behavior**:\n\n* Watches nodes matching `match` criteria.\n* Collects the specified address type from each node.\n* Updates the DNS A record with the collected addresses.\n\n---\n\n## Matching Behavior\n\nFLIPOP uses `spec.match` fields to determine which nodes receive Floating IPs or BYOIPs:\n\n1. **Pod Matching**: The controller watches pods in the specified `podNamespace` with labels matching `podLabel`. Only nodes running at least one matching pod are candidates.\n2. **Node Matching**: Nodes are filtered by `nodeLabel` and `tolerations`. If a node’s labels and taints match, it passes the node filter.\n\n**Assignment Logic**:\n\n* On each reconciliation, the IP Controller collects all candidate nodes.\n* If the number of assigned IPs is less than `desiredIPs`, it assigns IPs to the top candidates (sorted by name) until the quota is met.\n* If nodes no longer host matching pods or no longer match node criteria, then the annotation is removed and any DNS records are updated.\n  * Note that the controller will only unassign a Floating IP address from a Droplet if that node no longer matches AND it needs to assign the Floating IP to another node. This means that if a Floating IP is no longer needed it will stay attached to a Droplet to avoid any costs associated with a unassigned Floating IP address.\n* Reassignments respect `assignmentCoolOffSeconds` to avoid rapid churn. \n* When assigning an IP, the controller:\n    1. Requests an available IP from the provider or uses an assigned one from its list.\n    2. Annotates the node with `flipop.digitalocean.com/ipv4-reserved-ip: \u003cIP\u003e`.\n    3. Optionally updates DNS via `dnsRecordSet`.\n\n---\n\n## Metrics\n\nFLIPOP exports Prometheus metrics for both controllers and underlying provider calls.\n\n### FloatingIPPool Controller Metrics\n\nCollected by `pkg/floatingip/metrics.go`:\n\n* `flipop_floatingippoolcontroller_node_status{namespace,name,provider,dns,status}`: Gauge of node counts by status (`available`, `assigned`).\n* `flipop_floatingippoolcontroller_ip_assignment_errors{namespace,name,ip,provider,dns}`: Counter of IP assignment failures.\n* `flipop_floatingippoolcontroller_ip_assignments{namespace,name,ip,provider,dns}`: Counter of successful assignments.\n* `flipop_floatingippoolcontroller_ip_node{namespace,name,ip,provider,dns,provider_id,node}`: Gauge mapping IP to node.\n* `flipop_floatingippoolcontroller_ip_state{namespace,name,ip,provider,dns,state}`: Gauge of each IP’s current state.\n* `flipop_floatingippoolcontroller_unfulfilled_ips{namespace,name,provider,dns}`: Gauge of desired minus actual acquired IPs.\n\n### NodeDNSRecordSet Controller Metrics\n\nExposed via `pkg/nodedns/metrics.go`:\n\n* `flipop_nodednsrecordset_records{namespace,name,provider,dns}`: Gauge of total DNS records managed.\n\n### Provider Call Metrics\n\nEach provider instruments calls in `pkg/provider/metrics.go`:\n\n* `flipop_\u003csubsystem\u003e_calls_total{provider,call,outcome,kind,namespace,name}`: Counter of provider API invocations, labeled by outcome (`success` or `error`).\n* `flipop_\u003csubsystem\u003e_call_duration_seconds{provider,call,kind,namespace,name}`: Histogram of call latencies.\n\n---\n\n## Providers\n\n| Provider     | IP Provider | DNS Provider | Configuration               |\n| ------------ | :---------: | :----------: | --------------------------- |\n| digitalocean |      ✅      |       ✅      | `DIGITALOCEAN_ACCESS_TOKEN` |\n| cloudflare   |      ❌      |       ✅      | `CLOUDFLARE_TOKEN`          |\n\nSet credentials as environment variables in your operator namespace.\n\n** Note: ** For large clusters, it's recommended to request an increase in your API rate limit to mitigate any API throttling due to DNS updates. Large number of DNS updates can be made during events, such as a cluster upgrade, where nodes matching status changes frequently. \n\n---\n\n## Installation\n\n   ```bash\n    kubectl create namespace flipop\n    kubectl create secret generic flipop -n flipop --from-literal=DIGITALOCEAN_ACCESS_TOKEN=\"CENSORED\"\n    kubectl apply -n flipop -f k8s\n   ```\n---\n\n## Why not operator-framework/kubebuilder?\n\nThis operator is concerned with the relationships between FloatingIPPool, Node, and Pod resources. The controller-runtime (leveraged by kubebuilder) and operator-framework assume related objects are owned by the controller objects. OwnerReferences trigger garbage collection, which is a non-starter for this use-case. Deleting a FloatingIPPool shouldn't delete the Pods and Nodes its concerned with. The controller-runtime also assumes we're interested in all resources we \"own\". While controllers can be constrained with label selectors and namespaces, controllers can only be added to manager, not removed. In the case of this controller, we're likely only interested a small subset of pods and nodes, but those subscriptions may change based upon the definition in the FloatingIPPool resource.\n\n---\n\n## TODO\n- __Grace-periods__ - Moving IPs has a cost. It breaks all active connections, has a momentary period where connections will fail, and risks errors.  In some cases it may be better to give the node a chance to recover.\n\n---\n\n## Bugs / PRs / Contributing\n\nAt DigitalOcean we value and love our community! If you have any issues or would like to contribute, see [CONTRIBUTING.md](CONTRIBUTING.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdigitalocean%2Fflipop","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdigitalocean%2Fflipop","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdigitalocean%2Fflipop/lists"}