Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a26a28876 | ||
|
|
d2b1a21a07 | ||
|
|
8718d27fc7 | ||
|
|
f991c2fe15 | ||
|
|
cf30022133 |
@@ -1,4 +0,0 @@
|
||||
owner: KubelanCloud
|
||||
git-repo: kks-provider-plugin
|
||||
pages-branch: gh-pages
|
||||
pages-index-path: index.yaml
|
||||
@@ -20,7 +20,6 @@ jobs:
|
||||
|
||||
- name: Container image name
|
||||
run: |
|
||||
owner=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
|
||||
echo "IMAGE_NAME=${REGISTRY_HOST}/kloude-public/kks-provider-plugin" >> "$GITHUB_ENV"
|
||||
echo "APP_VERSION=$(grep '^appVersion:' charts/kks-provider-plugin/Chart.yaml | awk '{print $2}' | tr -d '\"')" >> "$GITHUB_ENV"
|
||||
|
||||
@@ -52,11 +51,3 @@ jobs:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
- name: Make container image public
|
||||
if: github.event_name != 'pull_request'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
owner=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
|
||||
gh api --method PATCH "/orgs/${owner}/packages/container/kks-provider-plugin/visibility" -f visibility=public || true
|
||||
|
||||
@@ -117,9 +117,3 @@ jobs:
|
||||
echo "OCI push failed for $chart (registry token realm/protocol issue). Continuing because chart artifact is already published to Gitea release assets."
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Make OCI chart public
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh api --method PATCH "/orgs/kloude/kks-provider-plugin/packages/container/charts%2Fkks-provider-plugin/visibility" -f visibility=public || true
|
||||
|
||||
@@ -48,52 +48,8 @@ jobs:
|
||||
helm package "${chart_dir}" --destination dist
|
||||
echo "CHART_PACKAGE=dist/kks-provider-plugin-${version}.tgz" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build changelog
|
||||
id: changelog
|
||||
uses: mikepenz/release-changelog-builder-action@v5
|
||||
with:
|
||||
configurationJson: |
|
||||
{
|
||||
"template": "#{{CHANGELOG}}\n\n**Full Changelog**: #{{RELEASE_DIFF}}",
|
||||
"categories": [
|
||||
{
|
||||
"title": "## Features",
|
||||
"commits": ["^feat", "^feature"]
|
||||
},
|
||||
{
|
||||
"title": "## Bug Fixes",
|
||||
"commits": ["^fix", "^Fix"]
|
||||
},
|
||||
{
|
||||
"title": "## Other Changes",
|
||||
"commits": [".*"]
|
||||
}
|
||||
]
|
||||
}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create or update GitHub release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
|
||||
run: |
|
||||
if gh release view "$TAG" >/dev/null 2>&1; then
|
||||
gh release edit "$TAG" --notes "$CHANGELOG"
|
||||
else
|
||||
gh release create "$TAG" --title "$TAG" --notes "$CHANGELOG"
|
||||
fi
|
||||
|
||||
- name: Upload Helm chart to release
|
||||
if: steps.version.outputs.is_app_release == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: gh release upload "$TAG" "$CHART_PACKAGE" --clobber
|
||||
|
||||
- name: Upload Helm chart artifact to Gitea release
|
||||
if: steps.version.outputs.is_app_release == 'true' && secrets.GITEA_TOKEN != ''
|
||||
- name: Create or update Gitea release
|
||||
if: secrets.GITEA_TOKEN != ''
|
||||
env:
|
||||
GITEA_BASE_URL: https://git.kloude.ir
|
||||
GITEA_REPO: kloude/kks-provider-plugin
|
||||
@@ -102,13 +58,9 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ ! -f "$CHART_PACKAGE" ]; then
|
||||
echo "Chart package not found: $CHART_PACKAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
api="${GITEA_BASE_URL}/api/v1/repos/${GITEA_REPO}"
|
||||
file_name=$(basename "$CHART_PACKAGE")
|
||||
release_name="$TAG"
|
||||
release_body="Release ${TAG}"
|
||||
|
||||
release_json=$(curl -fsS \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
@@ -119,10 +71,30 @@ jobs:
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${api}/releases" \
|
||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${TAG}\",\"target_commitish\":\"${GITHUB_SHA}\"}")
|
||||
-d "{\"tag_name\":\"${TAG}\",\"name\":\"${release_name}\",\"body\":\"${release_body}\",\"target_commitish\":\"${GITHUB_SHA}\"}")
|
||||
else
|
||||
release_id=$(python3 -c 'import json,sys; print(json.loads(sys.stdin.read())["id"])' <<< "$release_json")
|
||||
curl -fsS -X PATCH \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
"${api}/releases/${release_id}" \
|
||||
-d "{\"name\":\"${release_name}\",\"body\":\"${release_body}\"}" >/dev/null
|
||||
release_json=$(curl -fsS \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
"${api}/releases/tags/${TAG}")
|
||||
fi
|
||||
|
||||
if [ "${{ steps.version.outputs.is_app_release }}" != "true" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "$CHART_PACKAGE" ]; then
|
||||
echo "Chart package not found: $CHART_PACKAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release_id=$(python3 -c 'import json,sys; print(json.loads(sys.stdin.read())["id"])' <<< "$release_json")
|
||||
file_name=$(basename "$CHART_PACKAGE")
|
||||
|
||||
existing_assets=$(curl -fsS \
|
||||
-H "Authorization: token ${GITEA_TOKEN}" \
|
||||
@@ -154,11 +126,3 @@ jobs:
|
||||
"$CHART_PACKAGE" "$repo"; then
|
||||
echo "OCI push failed (registry token realm/protocol issue). Continuing because chart artifact is already published to Gitea release assets."
|
||||
fi
|
||||
|
||||
- name: Make OCI chart public
|
||||
if: steps.version.outputs.is_app_release == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
owner=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
|
||||
gh api --method PATCH "/orgs/${owner}/packages/container/charts%2Fkks-provider-plugin/visibility" -f visibility=public || true
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
apiVersion: v2
|
||||
name: kks-provider-plugin
|
||||
description: Combined Kloud provider plugin chart (LoadBalancer + CSI) for kks clusters
|
||||
description: Combined Kloude provider plugin chart (LoadBalancer + CSI) for kks clusters
|
||||
type: application
|
||||
version: 1.1.2
|
||||
appVersion: "1.1.0"
|
||||
version: 1.2.6
|
||||
appVersion: "1.2.0"
|
||||
kubeVersion: ">=1.28.0-0"
|
||||
home: https://github.com/KubelanCloud/kks-provider-plugin
|
||||
home: https://git.kloude.ir/kloude/kks-provider-plugin
|
||||
sources:
|
||||
- https://github.com/KubelanCloud/kks-provider-plugin
|
||||
- https://git.kloude.ir/kloude/kks-provider-plugin
|
||||
keywords:
|
||||
- provider
|
||||
- loadbalancer
|
||||
- csi
|
||||
- storage
|
||||
- kloud
|
||||
- kloude
|
||||
maintainers:
|
||||
- name: Kloud Team
|
||||
- name: Kloude Tech Team
|
||||
|
||||
@@ -6,7 +6,7 @@ nameOverride: ""
|
||||
fullnameOverride: ""
|
||||
|
||||
image:
|
||||
repository: registry.kloude.ir/kks/kubelancloud/kks-provider-plugin
|
||||
repository: registry.kloude.ir/kloude-public/kks-provider-plugin:latest
|
||||
tag: "latest"
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
|
||||
@@ -3,7 +3,9 @@ package kloudlb
|
||||
const (
|
||||
Finalizer = "lb.kloude.ir/finalizer"
|
||||
AnnotationIP = "lb.kloude.ir/ip"
|
||||
AnnotationVIP = "lb.kloude.ir/vip"
|
||||
AnnotationLBID = "lb.kloude.ir/id"
|
||||
AnnotationNode = "lb.kloude.ir/node"
|
||||
|
||||
Namespace = "kube-system"
|
||||
)
|
||||
|
||||
@@ -3,6 +3,7 @@ package controller
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -14,9 +15,9 @@ import (
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
corelisters "k8s.io/client-go/listers/core/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/tools/leaderelection"
|
||||
"k8s.io/client-go/tools/leaderelection/resourcelock"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
|
||||
"github.com/KubelanCloud/kks-provider-plugin/pkg/kloudlb"
|
||||
@@ -170,23 +171,33 @@ func (c *Controller) sync(ctx context.Context, key string) error {
|
||||
}
|
||||
|
||||
lb, err := c.lbClient.Allocate(ctx, provisioner.AllocateRequest{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
Ports: ports,
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
Ports: ports,
|
||||
LoadBalancerSourceRanges: serviceLoadBalancerSourceRanges(svc),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
vip := strings.TrimSpace(lb.IP)
|
||||
ingressIP, err := vipIngressIP(vip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
patch := svc.DeepCopy()
|
||||
if patch.Annotations == nil {
|
||||
patch.Annotations = map[string]string{}
|
||||
}
|
||||
patch.Annotations[kloudlb.AnnotationIP] = lb.IP
|
||||
patch.Annotations[kloudlb.AnnotationIP] = ingressIP
|
||||
patch.Annotations[kloudlb.AnnotationVIP] = vip
|
||||
patch.Annotations[kloudlb.AnnotationLBID] = lb.ID
|
||||
if node := strings.TrimSpace(lb.Node); node != "" {
|
||||
patch.Annotations[kloudlb.AnnotationNode] = node
|
||||
}
|
||||
patch.Status = corev1.ServiceStatus{
|
||||
LoadBalancer: corev1.LoadBalancerStatus{
|
||||
Ingress: []corev1.LoadBalancerIngress{{IP: lb.IP}},
|
||||
Ingress: []corev1.LoadBalancerIngress{{IP: ingressIP}},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -194,6 +205,29 @@ func (c *Controller) sync(ctx context.Context, key string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func vipIngressIP(vip string) (string, error) {
|
||||
vip = strings.TrimSpace(vip)
|
||||
if vip == "" {
|
||||
return "", fmt.Errorf("load balancer vip is empty")
|
||||
}
|
||||
if strings.Contains(vip, "/") {
|
||||
ip, _, err := net.ParseCIDR(vip)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid load balancer vip %q: %w", vip, err)
|
||||
}
|
||||
ipv4 := ip.To4()
|
||||
if ipv4 == nil {
|
||||
return "", fmt.Errorf("load balancer vip %q is not ipv4", vip)
|
||||
}
|
||||
return ipv4.String(), nil
|
||||
}
|
||||
ip := net.ParseIP(vip)
|
||||
if ip == nil || ip.To4() == nil {
|
||||
return "", fmt.Errorf("invalid load balancer vip %q", vip)
|
||||
}
|
||||
return ip.String(), nil
|
||||
}
|
||||
|
||||
func (c *Controller) finalize(ctx context.Context, svc *corev1.Service) error {
|
||||
if !containsString(svc.Finalizers, kloudlb.Finalizer) {
|
||||
return nil
|
||||
@@ -246,6 +280,24 @@ func servicePortsToAllocateRules(svc *corev1.Service) ([]provisioner.AllocatePor
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func serviceLoadBalancerSourceRanges(svc *corev1.Service) []string {
|
||||
if svc == nil || len(svc.Spec.LoadBalancerSourceRanges) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(svc.Spec.LoadBalancerSourceRanges))
|
||||
for _, item := range svc.Spec.LoadBalancerSourceRanges {
|
||||
trimmed := strings.TrimSpace(item)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, trimmed)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func containsString(items []string, target string) bool {
|
||||
for _, item := range items {
|
||||
if item == target {
|
||||
|
||||
@@ -45,3 +45,20 @@ func TestServicePortsToAllocateRulesRejectsNoPorts(t *testing.T) {
|
||||
t.Fatal("expected error for empty ports")
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceLoadBalancerSourceRanges(t *testing.T) {
|
||||
svc := &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "web"},
|
||||
Spec: corev1.ServiceSpec{
|
||||
LoadBalancerSourceRanges: []string{"203.0.113.0/24", " 198.51.100.8/32 ", ""},
|
||||
},
|
||||
}
|
||||
|
||||
ranges := serviceLoadBalancerSourceRanges(svc)
|
||||
if len(ranges) != 2 {
|
||||
t.Fatalf("ranges count = %d, want 2", len(ranges))
|
||||
}
|
||||
if ranges[0] != "203.0.113.0/24" || ranges[1] != "198.51.100.8/32" {
|
||||
t.Fatalf("unexpected ranges: %#v", ranges)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
coordinationv1 "k8s.io/api/coordination/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
@@ -141,26 +142,42 @@ func (s *Speaker) sync(ctx context.Context, key string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
ip := serviceExternalIP(svc)
|
||||
if ip == "" || svc.DeletionTimestamp != nil {
|
||||
s.stopLeading(key, ip)
|
||||
vip := serviceExternalVIP(svc)
|
||||
if vip == "" || svc.DeletionTimestamp != nil {
|
||||
s.stopLeading(key, vip)
|
||||
return nil
|
||||
}
|
||||
if owner := serviceOwnerNode(svc); owner != "" && owner != s.nodeName {
|
||||
s.stopLeading(key, vip)
|
||||
return nil
|
||||
}
|
||||
|
||||
leaseName := leaseNameFor(key)
|
||||
leader, err := s.acquireLease(ctx, leaseName, ip)
|
||||
leader, err := s.acquireLease(ctx, leaseName, vip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if leader != s.nodeName {
|
||||
s.stopLeading(key, ip)
|
||||
s.stopLeading(key, vip)
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.ensureVIP(ip)
|
||||
return s.ensureVIP(vip)
|
||||
}
|
||||
|
||||
func serviceExternalIP(svc *corev1.Service) string {
|
||||
func serviceOwnerNode(svc *corev1.Service) string {
|
||||
if svc == nil || svc.Annotations == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(svc.Annotations[kloudlb.AnnotationNode])
|
||||
}
|
||||
|
||||
func serviceExternalVIP(svc *corev1.Service) string {
|
||||
if svc.Annotations != nil {
|
||||
if vip := strings.TrimSpace(svc.Annotations[kloudlb.AnnotationVIP]); vip != "" {
|
||||
return vip
|
||||
}
|
||||
}
|
||||
for _, ing := range svc.Status.LoadBalancer.Ingress {
|
||||
if ing.IP != "" {
|
||||
return ing.IP
|
||||
@@ -287,17 +304,15 @@ func addVIP(ifaceName, ip string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("lookup interface %s: %w", ifaceName, err)
|
||||
}
|
||||
parsed := net.ParseIP(ip)
|
||||
if parsed == nil {
|
||||
return fmt.Errorf("invalid vip %q", ip)
|
||||
}
|
||||
addr := &netlink.Addr{
|
||||
IPNet: &net.IPNet{IP: parsed.To4(), Mask: net.CIDRMask(32, 32)},
|
||||
ipNet, err := vipToIPNet(link, ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr := &netlink.Addr{IPNet: ipNet}
|
||||
if err := netlink.AddrAdd(link, addr); err != nil {
|
||||
return fmt.Errorf("add vip %s on %s: %w", ip, ifaceName, err)
|
||||
}
|
||||
return sendGratuitousARP(link, parsed.To4())
|
||||
return sendGratuitousARP(link, ipNet.IP.To4())
|
||||
}
|
||||
|
||||
func removeVIP(ifaceName, ip string) error {
|
||||
@@ -305,16 +320,63 @@ func removeVIP(ifaceName, ip string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parsed := net.ParseIP(ip)
|
||||
if parsed == nil {
|
||||
return fmt.Errorf("invalid vip %q", ip)
|
||||
}
|
||||
addr := &netlink.Addr{
|
||||
IPNet: &net.IPNet{IP: parsed.To4(), Mask: net.CIDRMask(32, 32)},
|
||||
ipNet, err := vipToIPNet(link, ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr := &netlink.Addr{IPNet: ipNet}
|
||||
return netlink.AddrDel(link, addr)
|
||||
}
|
||||
|
||||
func vipToIPNet(link netlink.Link, vip string) (*net.IPNet, error) {
|
||||
vip = strings.TrimSpace(vip)
|
||||
if vip == "" {
|
||||
return nil, fmt.Errorf("vip is required")
|
||||
}
|
||||
if strings.Contains(vip, "/") {
|
||||
ip, ipNet, err := net.ParseCIDR(vip)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid vip %q: %w", vip, err)
|
||||
}
|
||||
ipv4 := ip.To4()
|
||||
if ipv4 == nil {
|
||||
return nil, fmt.Errorf("vip %q is not ipv4", vip)
|
||||
}
|
||||
return &net.IPNet{IP: ipv4, Mask: ipNet.Mask}, nil
|
||||
}
|
||||
|
||||
ip := net.ParseIP(vip)
|
||||
if ip == nil || ip.To4() == nil {
|
||||
return nil, fmt.Errorf("invalid vip %q", vip)
|
||||
}
|
||||
mask, err := interfaceIPv4Mask(link)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &net.IPNet{IP: ip.To4(), Mask: mask}, nil
|
||||
}
|
||||
|
||||
func interfaceIPv4Mask(link netlink.Link) (net.IPMask, error) {
|
||||
addrs, err := netlink.AddrList(link, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list ipv4 addresses on %s: %w", link.Attrs().Name, err)
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if addr.IPNet == nil || addr.IPNet.IP == nil || addr.IPNet.Mask == nil {
|
||||
continue
|
||||
}
|
||||
ip := addr.IPNet.IP.To4()
|
||||
if ip == nil {
|
||||
continue
|
||||
}
|
||||
if !ip.IsGlobalUnicast() {
|
||||
continue
|
||||
}
|
||||
return addr.IPNet.Mask, nil
|
||||
}
|
||||
return nil, fmt.Errorf("cannot infer ipv4 mask from interface %s", link.Attrs().Name)
|
||||
}
|
||||
|
||||
func sendGratuitousARP(link netlink.Link, ip net.IP) error {
|
||||
if ip == nil {
|
||||
return fmt.Errorf("ip is required")
|
||||
|
||||
@@ -5,12 +5,14 @@ type LoadBalancer struct {
|
||||
IP string `json:"ip"`
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
Node string `json:"node,omitempty"`
|
||||
}
|
||||
|
||||
type AllocateRequest struct {
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
Ports []AllocatePortRule `json:"ports,omitempty"`
|
||||
Namespace string `json:"namespace"`
|
||||
Name string `json:"name"`
|
||||
Ports []AllocatePortRule `json:"ports,omitempty"`
|
||||
LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"`
|
||||
}
|
||||
|
||||
type AllocatePortRule struct {
|
||||
|
||||
Reference in New Issue
Block a user