Author SHA1 Message Date
josh 8718d27fc7 feat(lb): honor lb vip mask from api
deploy / build (push) Successful in 2m40s
Release Chart / release (push) Successful in 5s
2026-08-01 21:04:58 +03:30
josh f991c2fe15 feat: update
Release Chart / release (push) Successful in 5s
deploy / build (push) Successful in 2m29s
2026-08-01 12:26:59 +03:30
josh cf30022133 ci: switch release pipelines to gitea only
Release Chart / release (push) Successful in 5s
Release / release (push) Successful in 4s
deploy / build (push) Successful in 2m27s
2026-08-01 07:45:36 +03:30
8 changed files with 134 additions and 103 deletions
-9
View File
@@ -20,7 +20,6 @@ jobs:
- name: Container image name - name: Container image name
run: | run: |
owner=$(echo "${GITHUB_REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')
echo "IMAGE_NAME=${REGISTRY_HOST}/kloude-public/kks-provider-plugin" >> "$GITHUB_ENV" 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" 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' }} push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} 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
-6
View File
@@ -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." echo "OCI push failed for $chart (registry token realm/protocol issue). Continuing because chart artifact is already published to Gitea release assets."
fi fi
done 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
+25 -61
View File
@@ -48,52 +48,8 @@ jobs:
helm package "${chart_dir}" --destination dist helm package "${chart_dir}" --destination dist
echo "CHART_PACKAGE=dist/kks-provider-plugin-${version}.tgz" >> "$GITHUB_ENV" echo "CHART_PACKAGE=dist/kks-provider-plugin-${version}.tgz" >> "$GITHUB_ENV"
- name: Build changelog - name: Create or update Gitea release
id: changelog if: secrets.GITEA_TOKEN != ''
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 != ''
env: env:
GITEA_BASE_URL: https://git.kloude.ir GITEA_BASE_URL: https://git.kloude.ir
GITEA_REPO: kloude/kks-provider-plugin GITEA_REPO: kloude/kks-provider-plugin
@@ -102,13 +58,9 @@ jobs:
run: | run: |
set -euo pipefail 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}" 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 \ release_json=$(curl -fsS \
-H "Authorization: token ${GITEA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
@@ -119,10 +71,30 @@ jobs:
-H "Authorization: token ${GITEA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
"${api}/releases" \ "${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 fi
release_id=$(python3 -c 'import json,sys; print(json.loads(sys.stdin.read())["id"])' <<< "$release_json") 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 \ existing_assets=$(curl -fsS \
-H "Authorization: token ${GITEA_TOKEN}" \ -H "Authorization: token ${GITEA_TOKEN}" \
@@ -154,11 +126,3 @@ jobs:
"$CHART_PACKAGE" "$repo"; then "$CHART_PACKAGE" "$repo"; then
echo "OCI push failed (registry token realm/protocol issue). Continuing because chart artifact is already published to Gitea release assets." echo "OCI push failed (registry token realm/protocol issue). Continuing because chart artifact is already published to Gitea release assets."
fi 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
+2 -2
View File
@@ -2,8 +2,8 @@ apiVersion: v2
name: kks-provider-plugin name: kks-provider-plugin
description: Combined Kloud provider plugin chart (LoadBalancer + CSI) for kks clusters description: Combined Kloud provider plugin chart (LoadBalancer + CSI) for kks clusters
type: application type: application
version: 1.1.2 version: 1.2.5
appVersion: "1.1.0" appVersion: "1.1.2"
kubeVersion: ">=1.28.0-0" kubeVersion: ">=1.28.0-0"
home: https://github.com/KubelanCloud/kks-provider-plugin home: https://github.com/KubelanCloud/kks-provider-plugin
sources: sources:
+1 -1
View File
@@ -6,7 +6,7 @@ nameOverride: ""
fullnameOverride: "" fullnameOverride: ""
image: image:
repository: registry.kloude.ir/kks/kubelancloud/kks-provider-plugin repository: registry.kloude.ir/kloude-public/kks-provider-plugin:latest
tag: "latest" tag: "latest"
pullPolicy: IfNotPresent pullPolicy: IfNotPresent
+1
View File
@@ -3,6 +3,7 @@ package kloudlb
const ( const (
Finalizer = "lb.kloude.ir/finalizer" Finalizer = "lb.kloude.ir/finalizer"
AnnotationIP = "lb.kloude.ir/ip" AnnotationIP = "lb.kloude.ir/ip"
AnnotationVIP = "lb.kloude.ir/vip"
AnnotationLBID = "lb.kloude.ir/id" AnnotationLBID = "lb.kloude.ir/id"
Namespace = "kube-system" Namespace = "kube-system"
+33 -3
View File
@@ -3,6 +3,7 @@ package controller
import ( import (
"context" "context"
"fmt" "fmt"
"net"
"os" "os"
"strings" "strings"
"time" "time"
@@ -14,9 +15,9 @@ import (
"k8s.io/client-go/informers" "k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
corelisters "k8s.io/client-go/listers/core/v1" 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"
"k8s.io/client-go/tools/leaderelection/resourcelock" "k8s.io/client-go/tools/leaderelection/resourcelock"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/util/workqueue" "k8s.io/client-go/util/workqueue"
"github.com/KubelanCloud/kks-provider-plugin/pkg/kloudlb" "github.com/KubelanCloud/kks-provider-plugin/pkg/kloudlb"
@@ -177,16 +178,22 @@ func (c *Controller) sync(ctx context.Context, key string) error {
if err != nil { if err != nil {
return err return err
} }
vip := strings.TrimSpace(lb.IP)
ingressIP, err := vipIngressIP(vip)
if err != nil {
return err
}
patch := svc.DeepCopy() patch := svc.DeepCopy()
if patch.Annotations == nil { if patch.Annotations == nil {
patch.Annotations = map[string]string{} 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 patch.Annotations[kloudlb.AnnotationLBID] = lb.ID
patch.Status = corev1.ServiceStatus{ patch.Status = corev1.ServiceStatus{
LoadBalancer: corev1.LoadBalancerStatus{ LoadBalancer: corev1.LoadBalancerStatus{
Ingress: []corev1.LoadBalancerIngress{{IP: lb.IP}}, Ingress: []corev1.LoadBalancerIngress{{IP: ingressIP}},
}, },
} }
@@ -194,6 +201,29 @@ func (c *Controller) sync(ctx context.Context, key string) error {
return err 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 { func (c *Controller) finalize(ctx context.Context, svc *corev1.Service) error {
if !containsString(svc.Finalizers, kloudlb.Finalizer) { if !containsString(svc.Finalizers, kloudlb.Finalizer) {
return nil return nil
+72 -21
View File
@@ -5,11 +5,12 @@ import (
"fmt" "fmt"
"net" "net"
"os" "os"
"strings"
"sync" "sync"
"time" "time"
corev1 "k8s.io/api/core/v1"
coordinationv1 "k8s.io/api/coordination/v1" coordinationv1 "k8s.io/api/coordination/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait" "k8s.io/apimachinery/pkg/util/wait"
@@ -141,26 +142,31 @@ func (s *Speaker) sync(ctx context.Context, key string) error {
return err return err
} }
ip := serviceExternalIP(svc) vip := serviceExternalVIP(svc)
if ip == "" || svc.DeletionTimestamp != nil { if vip == "" || svc.DeletionTimestamp != nil {
s.stopLeading(key, ip) s.stopLeading(key, vip)
return nil return nil
} }
leaseName := leaseNameFor(key) leaseName := leaseNameFor(key)
leader, err := s.acquireLease(ctx, leaseName, ip) leader, err := s.acquireLease(ctx, leaseName, vip)
if err != nil { if err != nil {
return err return err
} }
if leader != s.nodeName { if leader != s.nodeName {
s.stopLeading(key, ip) s.stopLeading(key, vip)
return nil return nil
} }
return s.ensureVIP(ip) return s.ensureVIP(vip)
} }
func serviceExternalIP(svc *corev1.Service) string { 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 { for _, ing := range svc.Status.LoadBalancer.Ingress {
if ing.IP != "" { if ing.IP != "" {
return ing.IP return ing.IP
@@ -287,17 +293,15 @@ func addVIP(ifaceName, ip string) error {
if err != nil { if err != nil {
return fmt.Errorf("lookup interface %s: %w", ifaceName, err) return fmt.Errorf("lookup interface %s: %w", ifaceName, err)
} }
parsed := net.ParseIP(ip) ipNet, err := vipToIPNet(link, ip)
if parsed == nil { if err != nil {
return fmt.Errorf("invalid vip %q", ip) return err
}
addr := &netlink.Addr{
IPNet: &net.IPNet{IP: parsed.To4(), Mask: net.CIDRMask(32, 32)},
} }
addr := &netlink.Addr{IPNet: ipNet}
if err := netlink.AddrAdd(link, addr); err != nil { if err := netlink.AddrAdd(link, addr); err != nil {
return fmt.Errorf("add vip %s on %s: %w", ip, ifaceName, err) 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 { func removeVIP(ifaceName, ip string) error {
@@ -305,16 +309,63 @@ func removeVIP(ifaceName, ip string) error {
if err != nil { if err != nil {
return err return err
} }
parsed := net.ParseIP(ip) ipNet, err := vipToIPNet(link, ip)
if parsed == nil { if err != nil {
return fmt.Errorf("invalid vip %q", ip) return err
}
addr := &netlink.Addr{
IPNet: &net.IPNet{IP: parsed.To4(), Mask: net.CIDRMask(32, 32)},
} }
addr := &netlink.Addr{IPNet: ipNet}
return netlink.AddrDel(link, addr) 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 { func sendGratuitousARP(link netlink.Link, ip net.IP) error {
if ip == nil { if ip == nil {
return fmt.Errorf("ip is required") return fmt.Errorf("ip is required")