Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
FROM alpine:3.22
FROM alpine:3.23
RUN apk add --no-cache curl tini
COPY cloud-controller-manager /usr/local/bin/
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["cloud-controller-manager"]
CMD ["cloud-controller-manager"]
24 changes: 24 additions & 0 deletions Dockerfile.builder
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FROM golang:1.25-alpine AS builder

WORKDIR /workspace
COPY go.mod go.mod
COPY go.sum go.sum
RUN go mod download
COPY ./ ./
ARG GOPROXY
ARG OS
ARG ARCH
ARG ARM
ARG LDFLAGS
RUN CGO_ENABLED=0 GOOS=${OS} GOARCH=${ARCH} GOARM=${ARM} GOPROXY=${GOPROXY} \
go build \
-ldflags="-extldflags '-static' ${LDFLAGS}" \
-o=cloud-controller-manager \
github.com/UpCloudLtd/upcloud-cloud-controller-manager/cmd/upcloud-cloud-controller-manager

FROM alpine:3.23
RUN apk add --no-cache curl tini
WORKDIR /
COPY --from=builder /workspace/cloud-controller-manager /usr/local/bin/
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["cloud-controller-manager"]
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ ifeq ($(GOPROXY),)
GOPROXY := https://proxy.golang.org
endif
export GOPROXY

export LDFLAGS := "-w -s -X 'k8s.io/component-base/version/verflag.programName=UpCloud cloud controller manager' $(shell scripts/version.sh "ldflags")"
LDFLAGS ?= ""
## --------------------------------------
## Binaries
## --------------------------------------

.PHONY: manager
manager: ## Build cloud controller manager binary in local environment
CGO_ENABLED=0 GOPROXY=$(GOPROXY) go build -ldflags $(LDFLAGS) -o $(BIN_DIR)/cloud-controller-manager github.com/UpCloudLtd/upcloud-cloud-controller-manager/cmd/upcloud-cloud-controller-manager

.PHONY: test
test:
go test -race ./...
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
module github.com/UpCloudLtd/upcloud-cloud-controller-manager

go 1.24.6
go 1.25

require (
github.com/UpCloudLtd/upcloud-go-api/v8 v8.20.0
github.com/UpCloudLtd/upcloud-go-api/v8 v8.35.0
github.com/google/uuid v1.6.0
github.com/stretchr/testify v1.10.0
github.com/stretchr/testify v1.11.1
gopkg.in/yaml.v2 v2.4.0
k8s.io/api v0.31.10
k8s.io/apimachinery v0.31.10
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/UpCloudLtd/upcloud-go-api/v8 v8.20.0 h1:nx1lbPwbqRPTNCZx437a7PrmyQkVXkgnK3hYZi9QgPc=
github.com/UpCloudLtd/upcloud-go-api/v8 v8.20.0/go.mod h1:ImDdnWfVVM6WCRTrskGhAw2W1cRwu5IEkBw+9UCzAv8=
github.com/UpCloudLtd/upcloud-go-api/v8 v8.35.0 h1:AIt07ExXzCaC9YVszkVPT+CteoyXldw0C8DGUMxtjD4=
github.com/UpCloudLtd/upcloud-go-api/v8 v8.35.0/go.mod h1:sxG94uNhC31OQH+zK0RhZjVj+PdkhObsNAt5bvq2J8c=
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA=
Expand Down Expand Up @@ -162,8 +162,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
Expand Down
6 changes: 6 additions & 0 deletions internal/loadbalancer/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ func createLoadBalancerRequestsEqual(r1, r2 *request.CreateLoadBalancerRequest)
if !reflect.DeepEqual(r1.Backends, r2.Backends) {
return fieldValueNotEqualError("backends")
}
if !reflect.DeepEqual(r1.IPAddresses, r2.IPAddresses) {
return fieldValueNotEqualError("IP addresses")
}

return nil
}
Expand Down Expand Up @@ -120,6 +123,9 @@ func sortCreateLoadBalancerRequestSlices(r *request.CreateLoadBalancerRequest) {
slices.SortFunc(r.Backends, func(a, b request.LoadBalancerBackend) int {
return strings.Compare(a.Name, b.Name)
})
slices.SortFunc(r.IPAddresses, func(a, b request.LoadBalancerIPAddress) int {
return strings.Compare(a.NetworkName, b.NetworkName)
})
}

func lengthNotEqualError(field string) error {
Expand Down
17 changes: 17 additions & 0 deletions internal/loadbalancer/compare_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ func TestCreateLoadBalancerRequestsEqual(t *testing.T) {
r2.Name = "test-1"
require.Equal(t, fieldValueNotEqualError("name"), createLoadBalancerRequestsEqual(&r1, &r2))
})
t.Run("IPAddresses", func(t *testing.T) {
t.Parallel()
r1 := createRequest(id)
r2 := createRequest(id)
r2.IPAddresses[0].Address = "0.0.0.1"
require.Equal(t, fieldValueNotEqualError("IP addresses"), createLoadBalancerRequestsEqual(&r1, &r2))
})
}

func createRequest(id uuid.UUID) request.CreateLoadBalancerRequest {
Expand All @@ -68,6 +75,7 @@ func createRequest(id uuid.UUID) request.CreateLoadBalancerRequest {
Backends: backends(id),
Resolvers: resolvers(),
Labels: upcloudLabels(id),
IPAddresses: ipAddresses(),
}
}

Expand Down Expand Up @@ -192,3 +200,12 @@ func upcloudLabels(id uuid.UUID) []upcloud.Label {
},
}
}

func ipAddresses() []request.LoadBalancerIPAddress {
return []request.LoadBalancerIPAddress{
{
NetworkName: networkNamePublic,
Address: "0.0.0.0",
},
}
}
10 changes: 4 additions & 6 deletions internal/loadbalancer/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,10 @@ const (
// ServiceExternalTrafficPolicyLabelKey is a key for label that should store external traffic policy type as a value.
serviceExternalTrafficPolicyLabel string = "ccm_external_traffic_policy"

changesDetectedEventType string = "ChangesDetected"
noChangesDetectedEventType string = "NoChangesDetected"
newLoadBalancerEventType string = "NewLoadBalancer"
updateLoadBalancerEventType string = "UpdateLoadBalancer"
deleteLoadBalancerEventType string = "DeleteLoadBalancer"
nodeCountLimitReached string = "NodeCountLimitReached"
changesDetectedEventType string = "ChangesDetected"
noChangesDetectedEventType string = "NoChangesDetected"
newLoadBalancerEventType string = "NewLoadBalancer"
nodeCountLimitReached string = "NodeCountLimitReached"

loadBalancerNameMaxLength int = 64
loadBalancerIDMaxLength int = 36
Expand Down
2 changes: 1 addition & 1 deletion internal/loadbalancer/loadbalancer.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func (m *manager) ensureNewLoadBalancer(ctx context.Context, clusterName string,
m.eventRecorder.Event(service, v1.EventTypeNormal, newLoadBalancerEventType, "Creating load balancer")
lb, err := m.svc.CreateLoadBalancer(ctx, service, nodes, clusterName)
// Patch service object as soon as we have LB UUID so that we don't loose reference.
if lb.UUID != "" && !serviceHasAnnotation(service, loadBalancerIDAnnotation) {
if lb != nil && lb.UUID != "" && !serviceHasAnnotation(service, loadBalancerIDAnnotation) {
modifiedService := service.DeepCopy()
updateServiceAnnotations(modifiedService, lb)
if perr := m.patchService(ctx, service, modifiedService); perr != nil {
Expand Down
12 changes: 11 additions & 1 deletion internal/loadbalancer/loadbalancer_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type replaceLoadBalancerRequest struct {
Labels []upcloud.Label `json:"labels,omitempty"`
MaintenanceDOW upcloud.LoadBalancerMaintenanceDOW `json:"maintenance_dow,omitempty"`
MaintenanceTime string `json:"maintenance_time,omitempty"`
IPAddresses []request.LoadBalancerIPAddress `json:"ip_addresses"`
}

func (r *replaceLoadBalancerRequest) RequestURL() string {
Expand Down Expand Up @@ -60,7 +61,8 @@ func createLoadBalancerRequest(service *v1.Service, nodes []*v1.Node, plan upclo
},
},
ConfiguredStatus: upcloud.LoadBalancerConfiguredStatusStarted,
Resolvers: []request.LoadBalancerResolver{},
Resolvers: make([]request.LoadBalancerResolver, 0),
IPAddresses: make([]request.LoadBalancerIPAddress, 0),
}
r.Frontends = make([]request.LoadBalancerFrontend, len(service.Spec.Ports))
r.Backends = make([]request.LoadBalancerBackend, len(service.Spec.Ports))
Expand Down Expand Up @@ -220,6 +222,13 @@ func loadBalancerToCreateRequest(lb *upcloud.LoadBalancer) *request.CreateLoadBa
CacheInvalid: lb.Resolvers[i].CacheInvalid,
}
}
ipAddresses := make([]request.LoadBalancerIPAddress, len(lb.IPAddresses))
for i := range ipAddresses {
ipAddresses[i] = request.LoadBalancerIPAddress{
NetworkName: lb.IPAddresses[i].NetworkName,
Address: lb.IPAddresses[i].Address,
}
}
return &request.CreateLoadBalancerRequest{
Name: lb.Name,
Plan: lb.Plan,
Expand All @@ -233,5 +242,6 @@ func loadBalancerToCreateRequest(lb *upcloud.LoadBalancer) *request.CreateLoadBa
Labels: lb.Labels,
MaintenanceDOW: lb.MaintenanceDOW,
MaintenanceTime: lb.MaintenanceTime,
IPAddresses: ipAddresses,
}
}
118 changes: 91 additions & 27 deletions internal/loadbalancer/loadbalancer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package loadbalancer_test

import (
"context"
"encoding/json"
"strings"
"testing"
"time"
Expand All @@ -11,6 +12,7 @@ import (
"github.com/UpCloudLtd/upcloud-cloud-controller-manager/internal/mock"
"github.com/UpCloudLtd/upcloud-cloud-controller-manager/internal/utils"
"github.com/UpCloudLtd/upcloud-go-api/v8/upcloud"
"github.com/UpCloudLtd/upcloud-go-api/v8/upcloud/request"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
Expand All @@ -29,23 +31,13 @@ func TestLoadBalancer(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()

m, err := initManager(ctx)
m, err := newManager(ctx)
require.NoError(t, err)

service := v1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "test-service", Namespace: "default"},
}
nodes := []*v1.Node{
{
ObjectMeta: metav1.ObjectMeta{
Name: "node-1",
Annotations: map[string]string{
utils.PrivateNetworkUUIDAnnotation: uuid.NewString(),
},
},
Spec: v1.NodeSpec{ProviderID: serverUUID},
},
}
nodes := newNodes()

t.Run("EnsureLoadBalancer", func(t *testing.T) {
status, err := m.EnsureLoadBalancer(ctx, "", &service, nodes)
Expand Down Expand Up @@ -90,23 +82,56 @@ func TestLoadBalancer(t *testing.T) {
})
}

func initManager(ctx context.Context) (cloudprovider.LoadBalancer, error) {
client := mock.NewControllerClientBuilder().ClientOrDie("test-client")
upcs := mock.NewUpCloudService(
upcloud.ServerDetails{Server: upcloud.Server{UUID: uuid.NewString()}},
upcloud.ServerDetails{Server: upcloud.Server{UUID: uuid.NewString()}},
upcloud.ServerDetails{
Server: upcloud.Server{UUID: serverUUID, State: upcloud.ServerStateStopped},
Networking: upcloud.ServerNetworking{
Interfaces: upcloud.ServerInterfaceSlice{
{
Type: upcloud.NetworkTypePrivate,
IPAddresses: upcloud.IPAddressSlice{{Address: "10.0.0.10"}},
},
},
func TestLoadBalancerCustomConfig(t *testing.T) {
t.Parallel()

ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()

upcs := newUpCloudMockService()

m, err := newManagerWithService(ctx, upcs)
require.NoError(t, err)

config := &request.CreateLoadBalancerRequest{
IPAddresses: []request.LoadBalancerIPAddress{
{
NetworkName: "public-IPv4",
Address: "0.0.0.0",
},
},
)
}
service := v1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test-service",
Namespace: "default",
Annotations: map[string]string{
"service.beta.kubernetes.io/upcloud-load-balancer-config": loadBalancerRequestToJSON(t, config),
},
},
}
nodes := newNodes()

t.Run("IPAddresses", func(t *testing.T) {
_, err := m.EnsureLoadBalancer(ctx, "", &service, nodes)
require.NoError(t, err)
lbs, err := upcs.GetLoadBalancers(ctx, &request.GetLoadBalancersRequest{})
require.NoError(t, err)
require.Len(t, lbs, 1)
lb := lbs[0]
require.Len(t, lb.IPAddresses, 1)
require.Equal(t, config.IPAddresses[0].NetworkName, lb.IPAddresses[0].NetworkName)
require.Equal(t, config.IPAddresses[0].Address, lb.IPAddresses[0].Address)
})
}

func newManager(ctx context.Context) (cloudprovider.LoadBalancer, error) {
upcs := newUpCloudMockService()
return newManagerWithService(ctx, upcs)
}

func newManagerWithService(ctx context.Context, upcs loadbalancer.UpCloudService) (cloudprovider.LoadBalancer, error) {
client := mock.NewControllerClientBuilder().ClientOrDie("test-client")
if _, err := client.CoreV1().Services("default").Create(ctx, &v1.Service{
ObjectMeta: metav1.ObjectMeta{Name: "test-service"},
Spec: v1.ServiceSpec{
Expand All @@ -125,3 +150,42 @@ func initManager(ctx context.Context) (cloudprovider.LoadBalancer, error) {
log := logger.NewKlog()
return loadbalancer.NewLoadBalancerManager(loadBalancerService, config, client.CoreV1(), eventRecorder, log), nil
}

func newUpCloudMockService() *mock.UpCloudService {
return mock.NewUpCloudService(
upcloud.ServerDetails{Server: upcloud.Server{UUID: uuid.NewString()}},
upcloud.ServerDetails{Server: upcloud.Server{UUID: uuid.NewString()}},
upcloud.ServerDetails{
Server: upcloud.Server{UUID: serverUUID, State: upcloud.ServerStateStopped},
Networking: upcloud.ServerNetworking{
Interfaces: upcloud.ServerInterfaceSlice{
{
Type: upcloud.NetworkTypePrivate,
IPAddresses: upcloud.IPAddressSlice{{Address: "10.0.0.10"}},
},
},
},
},
)
}

func newNodes() []*v1.Node {
return []*v1.Node{
{
ObjectMeta: metav1.ObjectMeta{
Name: "node-1",
Annotations: map[string]string{
utils.PrivateNetworkUUIDAnnotation: uuid.NewString(),
},
},
Spec: v1.NodeSpec{ProviderID: serverUUID},
},
}
}

func loadBalancerRequestToJSON(t *testing.T, r *request.CreateLoadBalancerRequest) string {
t.Helper()
b, err := json.MarshalIndent(r, "", "\t")
require.NoError(t, err)
return string(b)
}
1 change: 1 addition & 0 deletions internal/loadbalancer/upcloud_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ func (u *upCloudLoadBalancer) UpdateLoadBalancer(ctx context.Context, lb *upclou
Resolvers: config.Resolvers,
MaintenanceDOW: config.MaintenanceDOW,
MaintenanceTime: config.MaintenanceTime,
IPAddresses: config.IPAddresses,
}
b, err := json.Marshal(r)
if err != nil {
Expand Down
Loading
Loading