Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: loadbalancer source range #611

Merged
merged 6 commits into from
Oct 25, 2024
Merged
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
25 changes: 23 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@
# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
VERSION ?= v1.0.0

# ENVTEST_K8S_VERSION specifies the Kubernetes version to be used
# during testing with the envtest environment. This ensures that
# the tests run against the correct API and behavior for the
# specific Kubernetes release being targeted (v1.31.0 in this case).
ENVTEST_K8S_VERSION = 1.31.0

# ENVTEST_VERSION defines the version of the setup-envtest binary
# used to manage and download the Kubernetes binaries (like etcd,
# kube-apiserver, and kubectl) required for testing. This version
# ensures compatibility with the selected Kubernetes version and
# must align closely with recent releases (release-0.19 is chosen here).
# Mismatches between these versions could result in compatibility issues.
ENVTEST_VERSION ?= release-0.19

# Image URL to use all building/pushing image targets
CONTAINER_REPOSITORY ?= docker.io/clastix/kamaji

Expand Down Expand Up @@ -34,6 +48,7 @@ HELM ?= $(LOCALBIN)/helm
KIND ?= $(LOCALBIN)/kind
KO ?= $(LOCALBIN)/ko
YQ ?= $(LOCALBIN)/yq
ENVTEST ?= $(LOCALBIN)/setup-envtest

all: build

Expand Down Expand Up @@ -95,6 +110,11 @@ apidocs-gen: $(APIDOCS_GEN) ## Download crdoc locally if necessary.
$(APIDOCS_GEN): $(LOCALBIN)
test -s $(LOCALBIN)/crdoc || GOBIN=$(LOCALBIN) go install fybrik.io/crdoc@latest

.PHONY: envtest
envtest: $(ENVTEST) ## Download envtest-setup locally if necessary.
$(ENVTEST): $(LOCALBIN)
test -s $(LOCALBIN)/setup-envtest || GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@$(ENVTEST_VERSION)

##@ Development

rbac: controller-gen yq
Expand All @@ -121,9 +141,10 @@ generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and
golint: golangci-lint ## Linting the code according to the styling guide.
$(GOLANGCI_LINT) run -c .golangci.yml

## Run unit tests (all tests except E2E).
.PHONY: test
test: ## Run unit tests (all tests except E2E).
@go test \
test: envtest ginkgo
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" $(GINKGO) -r -v --trace \
./api/... \
./cmd/... \
./internal/... \
Expand Down
54 changes: 54 additions & 0 deletions api/v1alpha1/suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0
package v1alpha1

import (
"path/filepath"
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
)

var (
cfg *rest.Config
k8sClient client.Client
testEnv *envtest.Environment
)

func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "TenantControlPlane Suite")
}

var _ = BeforeSuite(func() {
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{
filepath.Join("..", "..", "charts", "kamaji", "crds"),
// filepath.Join("../..", "chart", "kamaji", "crds"),
},
}

var err error
cfg, err = testEnv.Start()
Expect(err).ToNot(HaveOccurred())
Expect(cfg).ToNot(BeNil())

err = AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())

k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).ToNot(HaveOccurred())
Expect(k8sClient).ToNot(BeNil())
})

var _ = AfterSuite(func() {
By("tearing down the test environment")
err := testEnv.Stop()
Expect(err).ToNot(HaveOccurred())
})
10 changes: 10 additions & 0 deletions api/v1alpha1/tenantcontrolplane_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ import (

// NetworkProfileSpec defines the desired state of NetworkProfile.
type NetworkProfileSpec struct {
// LoadBalancerSourceRanges restricts the IP ranges that can access
// the LoadBalancer type Service. This field defines a list of IP
// address ranges (in CIDR format) that are allowed to access the service.
// If left empty, the service will allow traffic from all IP ranges (0.0.0.0/0).
// This feature is useful for restricting access to API servers or services
// to specific networks for security purposes.
// Example: {"192.168.1.0/24", "10.0.0.0/8"}
LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty"`
// Address where API server of will be exposed.
// In case of LoadBalancer Service, this can be empty in order to use the exposed IP provided by the cloud controller manager.
Address string `json:"address,omitempty"`
Expand Down Expand Up @@ -256,6 +264,8 @@ type AddonsSpec struct {
// TenantControlPlaneSpec defines the desired state of TenantControlPlane.
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStore) || has(self.dataStore)", message="unsetting the dataStore is not supported"
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.dataStoreSchema) || has(self.dataStoreSchema)", message="unsetting the dataStoreSchema is not supported"
// +kubebuilder:validation:XValidation:rule="!has(self.networkProfile.loadBalancerSourceRanges) || (size(self.networkProfile.loadBalancerSourceRanges) == 0 || self.controlPlane.service.serviceType == 'LoadBalancer')", message="LoadBalancer source ranges are supported only with LoadBalancer service type"

type TenantControlPlaneSpec struct {
// DataStore allows to specify a DataStore that should be used to store the Kubernetes data for the given Tenant Control Plane.
// This parameter is optional and acts as an override over the default one which is used by the Kamaji Operator.
Expand Down
78 changes: 78 additions & 0 deletions api/v1alpha1/tenantcontrolplane_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package v1alpha1

import (
"context"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var _ = Describe("Cluster controller", func() {
var (
ctx context.Context
tcp *TenantControlPlane
)

BeforeEach(func() {
ctx = context.Background()
tcp = &TenantControlPlane{
ObjectMeta: metav1.ObjectMeta{
Name: "tcp",
Namespace: "default",
},
Spec: TenantControlPlaneSpec{},
}
})

AfterEach(func() {
if err := k8sClient.Delete(ctx, tcp); err != nil && !apierrors.IsNotFound(err) {
Expect(err).NotTo(HaveOccurred())
}
})

Context("LoadBalancer Source Ranges", func() {
It("allows creation when no CIDR ranges are provided", func() {
tcp.Spec.ControlPlane.Service.ServiceType = ServiceTypeLoadBalancer

err := k8sClient.Create(ctx, tcp)
Expect(err).NotTo(HaveOccurred())
})

It("allows creation with an explicitly empty CIDR list", func() {
tcp.Spec.ControlPlane.Service.ServiceType = ServiceTypeLoadBalancer
tcp.Spec.NetworkProfile.LoadBalancerSourceRanges = []string{}

err := k8sClient.Create(ctx, tcp)
Expect(err).NotTo(HaveOccurred())
})

It("allows creation when service type is not LoadBalancer and it has an empty CIDR list", func() {
tcp.Spec.ControlPlane.Service.ServiceType = ServiceTypeNodePort

err := k8sClient.Create(ctx, tcp)
Expect(err).NotTo(HaveOccurred())
})

It("allows CIDR ranges when service type is LoadBalancer", func() {
tcp.Spec.ControlPlane.Service.ServiceType = ServiceTypeLoadBalancer
tcp.Spec.NetworkProfile.LoadBalancerSourceRanges = []string{"192.168.0.0/24"}

err := k8sClient.Create(ctx, tcp)
Expect(err).NotTo(HaveOccurred())
})

It("denies CIDR ranges when service type is not LoadBalancer", func() {
tcp.Spec.ControlPlane.Service.ServiceType = ServiceTypeNodePort
tcp.Spec.NetworkProfile.LoadBalancerSourceRanges = []string{"192.168.0.0/24"}

err := k8sClient.Create(ctx, tcp)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("LoadBalancer source ranges are supported only with LoadBalancer service type"))
})
})
})
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion charts/kamaji/crds/kamaji.clastix.io_tenantcontrolplanes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ spec:
metadata:
type: object
spec:
description: TenantControlPlaneSpec defines the desired state of TenantControlPlane.
properties:
addons:
description: Addons contain which addons are enabled
Expand Down Expand Up @@ -6564,6 +6563,18 @@ spec:
items:
type: string
type: array
loadBalancerSourceRanges:
description: |-
LoadBalancerSourceRanges restricts the IP ranges that can access
the LoadBalancer type Service. This field defines a list of IP
address ranges (in CIDR format) that are allowed to access the service.
If left empty, the service will allow traffic from all IP ranges (0.0.0.0/0).
This feature is useful for restricting access to API servers or services
to specific networks for security purposes.
Example: {"192.168.1.0/24", "10.0.0.0/8"}
items:
type: string
type: array
podCidr:
default: 10.244.0.0/16
description: CIDR for Kubernetes Pods
Expand All @@ -6587,6 +6598,8 @@ spec:
rule: '!has(oldSelf.dataStore) || has(self.dataStore)'
- message: unsetting the dataStoreSchema is not supported
rule: '!has(oldSelf.dataStoreSchema) || has(self.dataStoreSchema)'
- message: LoadBalancer source ranges are supported only with LoadBalancer service type
rule: '!has(self.networkProfile.loadBalancerSourceRanges) || (size(self.networkProfile.loadBalancerSourceRanges) == 0 || self.controlPlane.service.serviceType == ''LoadBalancer'')'
status:
description: TenantControlPlaneStatus defines the observed state of TenantControlPlane.
properties:
Expand Down
1 change: 1 addition & 0 deletions cmd/manager/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ func NewCmd(scheme *runtime.Scheme) *cobra.Command {
},
},
handlers.TenantControlPlaneServiceCIDR{},
handlers.TenantControlPlaneLoadBalancerSourceRanges{},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're able to achieve the same result with CEL we can decrease the validation at the webhook server, and Kamaji will benefit a lot!

},
routes.TenantControlPlaneTelemetry{}: {
handlers.TenantControlPlaneTelemetry{
Expand Down
17 changes: 15 additions & 2 deletions docs/content/reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,7 @@ TenantControlPlane is the Schema for the tenantcontrolplanes API.
<td><b><a href="#tenantcontrolplanespec">spec</a></b></td>
<td>object</td>
<td>
TenantControlPlaneSpec defines the desired state of TenantControlPlane.<br/>
<br/>
</td>
<td>false</td>
</tr><tr>
Expand All @@ -800,7 +800,7 @@ TenantControlPlane is the Schema for the tenantcontrolplanes API.



TenantControlPlaneSpec defines the desired state of TenantControlPlane.


<table>
<thead>
Expand Down Expand Up @@ -13942,6 +13942,19 @@ Use this field to add additional hostnames when exposing the Tenant Control Plan
<i>Default</i>: [10.96.0.10]<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>loadBalancerSourceRanges</b></td>
<td>[]string</td>
<td>
LoadBalancerSourceRanges restricts the IP ranges that can access
the LoadBalancer type Service. This field defines a list of IP
address ranges (in CIDR format) that are allowed to access the service.
If left empty, the service will allow traffic from all IP ranges (0.0.0.0/0).
This feature is useful for restricting access to API servers or services
to specific networks for security purposes.
Example: {"192.168.1.0/24", "10.0.0.0/8"}<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>podCidr</b></td>
<td>string</td>
Expand Down
4 changes: 3 additions & 1 deletion internal/resources/k8s_service_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ func (r *KubernetesServiceResource) mutate(ctx context.Context, tenantControlPla
switch tenantControlPlane.Spec.ControlPlane.Service.ServiceType {
case kamajiv1alpha1.ServiceTypeLoadBalancer:
r.resource.Spec.Type = corev1.ServiceTypeLoadBalancer

if len(tenantControlPlane.Spec.NetworkProfile.LoadBalancerSourceRanges) > 0 {
r.resource.Spec.LoadBalancerSourceRanges = tenantControlPlane.Spec.NetworkProfile.LoadBalancerSourceRanges
}
if len(address) > 0 {
r.resource.Spec.LoadBalancerIP = address
}
Expand Down
58 changes: 58 additions & 0 deletions internal/webhook/handlers/tcp_lb_src_ranges.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2022 Clastix Labs
// SPDX-License-Identifier: Apache-2.0

package handlers

import (
"context"
"fmt"
"net"

"gomodules.xyz/jsonpatch/v2"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

kamajiv1alpha1 "github.com/clastix/kamaji/api/v1alpha1"
"github.com/clastix/kamaji/internal/webhook/utils"
)

type TenantControlPlaneLoadBalancerSourceRanges struct{}

func (t TenantControlPlaneLoadBalancerSourceRanges) handle(tcp *kamajiv1alpha1.TenantControlPlane) error {
for _, sourceCIDR := range tcp.Spec.NetworkProfile.LoadBalancerSourceRanges {
_, _, err := net.ParseCIDR(sourceCIDR)
if err != nil {
return fmt.Errorf("invalid LoadBalancer source CIDR %s, %s", sourceCIDR, err.Error())
}
}

return nil
}

func (t TenantControlPlaneLoadBalancerSourceRanges) OnCreate(object runtime.Object) AdmissionResponse {
return func(context.Context, admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
tcp := object.(*kamajiv1alpha1.TenantControlPlane) //nolint:forcetypeassert

if err := t.handle(tcp); err != nil {
return nil, err
}

return nil, nil
}
}

func (t TenantControlPlaneLoadBalancerSourceRanges) OnDelete(runtime.Object) AdmissionResponse {
return utils.NilOp()
}

func (t TenantControlPlaneLoadBalancerSourceRanges) OnUpdate(object runtime.Object, _ runtime.Object) AdmissionResponse {
return func(ctx context.Context, req admission.Request) ([]jsonpatch.JsonPatchOperation, error) {
tcp := object.(*kamajiv1alpha1.TenantControlPlane) //nolint:forcetypeassert

if err := t.handle(tcp); err != nil {
return nil, err
}

return nil, nil
}
}
Loading
Loading