Add initial version with help and admission controller (#1)

- It adds a helm chart that will be hosted in the Github pages URL of this repository
- an admission controller operator 
- Basic documentation for installing the operator, configuring CRDs and example pod configuration
This commit is contained in:
M. Essam
2025-01-29 22:44:09 +01:00
committed by GitHub
parent 50aa9ada37
commit 64091a6439
45 changed files with 4638 additions and 9 deletions
+113
View File
@@ -0,0 +1,113 @@
package v1
import (
"context"
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
"github.com/google/uuid"
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
)
// nolint:unused
// log is for logging in this package.
var nbsetupkeylog = logf.Log.WithName("nbsetupkey-resource")
// SetupNBSetupKeyWebhookWithManager registers the webhook for NBSetupKey in the manager.
func SetupNBSetupKeyWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).For(&netbirdiov1.NBSetupKey{}).
WithValidator(&NBSetupKeyCustomValidator{client: mgr.GetClient()}).
Complete()
}
// NBSetupKeyCustomValidator struct is responsible for validating the NBSetupKey resource
// when it is created, updated, or deleted.
type NBSetupKeyCustomValidator struct {
client client.Client
}
var _ webhook.CustomValidator = &NBSetupKeyCustomValidator{}
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type NBSetupKey.
func (v *NBSetupKeyCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
nbSetupKey, ok := obj.(*netbirdiov1.NBSetupKey)
if !ok {
return nil, fmt.Errorf("expected a NBSetupKey object but got %T", obj)
}
nbsetupkeylog.Info("Validating NBSetupKey", "namespace", nbSetupKey.Namespace, "name", nbSetupKey.Name)
if nbSetupKey.Spec.SecretKeyRef.Name == "" {
return nil, fmt.Errorf("spec.secretKeyRef.name is required")
}
if nbSetupKey.Spec.SecretKeyRef.Key == "" {
return nil, fmt.Errorf("spec.secretKeyRef.key is required")
}
var secret corev1.Secret
err := v.client.Get(ctx, types.NamespacedName{Namespace: nbSetupKey.Namespace, Name: nbSetupKey.Spec.SecretKeyRef.Name}, &secret)
if err != nil {
if errors.IsNotFound(err) {
return admission.Warnings{fmt.Sprintf("secret %s/%s not found", nbSetupKey.Namespace, nbSetupKey.Spec.SecretKeyRef.Name)}, nil
}
return nil, err
}
uuidBytes, ok := secret.Data[nbSetupKey.Spec.SecretKeyRef.Key]
if !ok {
return admission.Warnings{fmt.Sprintf("key %s in secret %s/%s not found", nbSetupKey.Spec.SecretKeyRef.Key, nbSetupKey.Namespace, nbSetupKey.Spec.SecretKeyRef.Name)}, nil
}
_, err = uuid.Parse(string(uuidBytes))
if err != nil {
return admission.Warnings{fmt.Sprintf("setupkey %s in secret %s/%s is not a valid setup key", nbSetupKey.Spec.SecretKeyRef.Key, nbSetupKey.Namespace, nbSetupKey.Spec.SecretKeyRef.Name)}, nil
}
return nil, nil
}
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type NBSetupKey.
func (v *NBSetupKeyCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
return v.ValidateCreate(ctx, newObj)
}
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type NBSetupKey.
func (v *NBSetupKeyCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
nbSetupKey, ok := obj.(*netbirdiov1.NBSetupKey)
if !ok {
return nil, fmt.Errorf("expected a NBSetupKey object but got %T", obj)
}
nbsetupkeylog.Info("Validating NBSetupKey deletion", "namespace", nbSetupKey.Namespace, "name", nbSetupKey.Name)
var pods corev1.PodList
err := v.client.List(ctx, &pods, client.InNamespace(nbSetupKey.Namespace))
if err != nil {
return nil, err
}
//nolint:prealloc
var invalidPods []string
for _, p := range pods.Items {
// If annotation doesn't exist, or doesn't match NBSetupKey being deleted, ignore
if v, ok := p.Annotations[setupKeyAnnotation]; !ok || v != nbSetupKey.Name {
continue
}
invalidPods = append(invalidPods, p.Name)
}
if len(invalidPods) > 0 {
return nil, fmt.Errorf("NBSetupKey is in-use by %d pods: %s", len(invalidPods), strings.Join(invalidPods, ","))
}
return nil, nil
}
@@ -0,0 +1,124 @@
package v1
import (
"context"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
)
var _ = Describe("NBSetupKey Webhook", func() {
var (
obj *netbirdiov1.NBSetupKey
validator NBSetupKeyCustomValidator
resourceName = "test"
secret *corev1.Secret
)
BeforeEach(func() {
obj = &netbirdiov1.NBSetupKey{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
},
}
validator = NBSetupKeyCustomValidator{
client: k8sClient,
}
Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
})
AfterEach(func() {
})
Context("When creating or updating NBSetupKey under Validating Webhook", func() {
When("secretKeyRef is empty", func() {
It("Should fail", func() {
obj.Spec = netbirdiov1.NBSetupKeySpec{}
warnings, err := validator.ValidateCreate(context.Background(), obj)
Expect(err).To(HaveOccurred())
Expect(warnings).To(BeEmpty())
})
})
When("secret doesn't exist", func() {
It("Should fail", func() {
obj.Spec = netbirdiov1.NBSetupKeySpec{
SecretKeyRef: corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: resourceName,
},
Key: "setupkey",
},
}
warnings, err := validator.ValidateCreate(context.Background(), obj)
Expect(err).NotTo(HaveOccurred())
Expect(warnings).NotTo(BeEmpty())
})
})
Context("secret exists", Ordered, func() {
createSecret := func(secretkey, setupkey string) {
resource := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Namespace: "default",
Name: resourceName,
},
Data: map[string][]byte{
secretkey: []byte(setupkey),
},
}
secret = &corev1.Secret{}
err := k8sClient.Get(ctx, types.NamespacedName{Namespace: "default", Name: resourceName}, secret)
if err == nil {
Expect(k8sClient.Delete(ctx, secret)).To(Succeed())
}
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
}
BeforeEach(func() {
obj.Spec = netbirdiov1.NBSetupKeySpec{
SecretKeyRef: corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: resourceName,
},
Key: "setupkey",
},
}
})
When("secret key doesn't exist", func() {
It("Should fail", func() {
createSecret("wrongkey", "EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE")
warnings, err := validator.ValidateCreate(context.Background(), obj)
Expect(err).NotTo(HaveOccurred())
Expect(warnings).NotTo(BeEmpty())
})
})
When("setup key is invalid", func() {
It("Should fail", func() {
createSecret("setupkey", "EEEEEEEE")
warnings, err := validator.ValidateCreate(context.Background(), obj)
Expect(err).NotTo(HaveOccurred())
Expect(warnings).NotTo(BeEmpty())
})
})
When("setup key is valid", func() {
It("Should allow creation", func() {
createSecret("setupkey", "EEEEEEEE-EEEE-EEEE-EEEE-EEEEEEEEEEEE")
warnings, err := validator.ValidateCreate(context.Background(), obj)
Expect(err).NotTo(HaveOccurred())
Expect(warnings).To(BeEmpty())
})
})
})
})
})
+127
View File
@@ -0,0 +1,127 @@
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import (
"context"
"fmt"
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)
const (
setupKeyAnnotation = "netbird.io/setup-key"
)
// nolint:unused
// log is for logging in this package.
var podlog = logf.Log.WithName("pod-resource")
// SetupPodWebhookWithManager registers the webhook for Pod in the manager.
func SetupPodWebhookWithManager(mgr ctrl.Manager, managementURL, clientImage string) error {
return ctrl.NewWebhookManagedBy(mgr).For(&corev1.Pod{}).
WithDefaulter(&PodNetbirdInjector{
client: mgr.GetClient(),
managementURL: managementURL,
clientImage: clientImage,
}).
Complete()
}
// PodNetbirdInjector struct is responsible for setting default values on the custom resource of the
// Kind Pod when those are created or updated.
type PodNetbirdInjector struct {
client client.Client
managementURL string
clientImage string
}
var _ webhook.CustomDefaulter = &PodNetbirdInjector{}
// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Pod.
func (d *PodNetbirdInjector) Default(ctx context.Context, obj runtime.Object) error {
pod, ok := obj.(*corev1.Pod)
if !ok {
return fmt.Errorf("expected an Pod object but got %T", obj)
}
podlog.Info("Defaulting for Pod", "name", pod.GetName())
if pod.Annotations == nil || pod.Annotations[setupKeyAnnotation] == "" {
return nil
}
var nbSetupKey netbirdiov1.NBSetupKey
err := d.client.Get(ctx, types.NamespacedName{Namespace: pod.Namespace, Name: pod.Annotations[setupKeyAnnotation]}, &nbSetupKey)
if err != nil {
return err
}
ready := false
for _, c := range nbSetupKey.Status.Conditions {
if c.Type == netbirdiov1.Ready {
ready = c.Status == corev1.ConditionTrue
}
}
if !ready {
return fmt.Errorf("NBSetupKey is not ready")
}
managementURL := d.managementURL
if nbSetupKey.Spec.ManagementURL != "" {
managementURL = nbSetupKey.Spec.ManagementURL
}
pod.Spec.Containers = append(pod.Spec.Containers, corev1.Container{
Name: "netbird",
Image: d.clientImage,
Args: []string{
"--setup-key-file",
"/etc/nbkey",
"-m",
managementURL,
},
Env: []corev1.EnvVar{
{
Name: "NB_SETUP_KEY",
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &nbSetupKey.Spec.SecretKeyRef,
},
},
{
Name: "NB_MANAGEMENT_URL",
Value: managementURL,
},
},
SecurityContext: &corev1.SecurityContext{
Capabilities: &corev1.Capabilities{
Add: []corev1.Capability{
"NET_ADMIN",
},
},
},
})
return nil
}
+130
View File
@@ -0,0 +1,130 @@
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import (
"context"
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var _ = Describe("Pod Webhook", func() {
var (
obj *corev1.Pod
defaulter PodNetbirdInjector
)
BeforeEach(func() {
obj = &corev1.Pod{
ObjectMeta: v1.ObjectMeta{
Name: "test",
Namespace: "test",
Annotations: make(map[string]string),
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "test",
},
},
},
}
defaulter = PodNetbirdInjector{
client: k8sClient,
managementURL: "https://api.netbird.io",
clientImage: "netbirdio/netbird:latest",
}
Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
})
AfterEach(func() {
})
Context("When creating Pod without annotation", func() {
It("Should not modify anything", func() {
err := defaulter.Default(context.Background(), obj)
Expect(err).NotTo(HaveOccurred())
Expect(obj.Spec.Containers).To(HaveLen(1))
})
})
Context("When creating Pod with annotation", func() {
BeforeEach(func() {
obj.Annotations[setupKeyAnnotation] = "test"
})
When("NBSetupKey doesn't exist", func() {
It("Should fail", func() {
Expect(defaulter.Default(context.Background(), obj)).To(HaveOccurred())
Expect(obj.Spec.Containers).To(HaveLen(1))
})
})
When("NBSetupKey exists", Ordered, func() {
BeforeAll(func() {
sk := netbirdiov1.NBSetupKey{
ObjectMeta: v1.ObjectMeta{
Name: "test",
Namespace: "test",
},
Spec: netbirdiov1.NBSetupKeySpec{
SecretKeyRef: corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{
Name: "test",
},
Key: "test",
},
},
}
err := k8sClient.Create(context.Background(), &corev1.Namespace{
ObjectMeta: v1.ObjectMeta{
Name: "test",
},
})
Expect(err).NotTo(HaveOccurred())
err = k8sClient.Create(context.Background(), &sk)
Expect(err).NotTo(HaveOccurred())
sk.Status = netbirdiov1.NBSetupKeyStatus{
Conditions: []netbirdiov1.NBSetupKeyCondition{
{
Type: netbirdiov1.Ready,
Status: corev1.ConditionTrue,
},
},
}
err = k8sClient.Status().Update(context.Background(), &sk)
Expect(err).NotTo(HaveOccurred())
})
It("Should inject NB container", func() {
Expect(defaulter.Default(context.Background(), obj)).NotTo(HaveOccurred())
Expect(obj.Spec.Containers).To(HaveLen(2))
Expect(obj.Spec.Containers[1].Name).To(Equal("netbird"))
})
})
})
})
+176
View File
@@ -0,0 +1,176 @@
/*
Copyright 2025.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package v1
import (
"context"
"crypto/tls"
"fmt"
"net"
"os"
"path/filepath"
"testing"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
admissionv1 "k8s.io/api/admission/v1"
k8siov1 "k8s.io/api/core/v1"
apimachineryruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
netbirdiov1 "github.com/netbirdio/kubernetes-operator/api/v1"
// +kubebuilder:scaffold:imports
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
var (
ctx context.Context
cancel context.CancelFunc
k8sClient client.Client
cfg *rest.Config
testEnv *envtest.Environment
)
func TestAPIs(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Webhook Suite")
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
ctx, cancel = context.WithCancel(context.TODO())
var err error
scheme := apimachineryruntime.NewScheme()
err = k8siov1.AddToScheme(scheme)
Expect(err).NotTo(HaveOccurred())
err = admissionv1.AddToScheme(scheme)
Expect(err).NotTo(HaveOccurred())
err = netbirdiov1.AddToScheme(scheme)
Expect(err).NotTo(HaveOccurred())
// +kubebuilder:scaffold:scheme
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "helm", "netbird-operator", "crds")},
ErrorIfCRDPathMissing: false,
// WebhookInstallOptions: envtest.WebhookInstallOptions{
// Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")},
// },
}
// Retrieve the first found binary directory to allow running tests from IDEs
if getFirstFoundEnvTestBinaryDir() != "" {
testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
}
// cfg is defined in this file globally.
cfg, err = testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())
// start webhook server using Manager.
webhookInstallOptions := &testEnv.WebhookInstallOptions
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme,
WebhookServer: webhook.NewServer(webhook.Options{
Host: webhookInstallOptions.LocalServingHost,
Port: webhookInstallOptions.LocalServingPort,
CertDir: webhookInstallOptions.LocalServingCertDir,
}),
LeaderElection: false,
Metrics: metricsserver.Options{BindAddress: "0"},
})
Expect(err).NotTo(HaveOccurred())
err = SetupPodWebhookWithManager(mgr, "", "")
Expect(err).NotTo(HaveOccurred())
err = SetupNBSetupKeyWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())
// +kubebuilder:scaffold:webhook
go func() {
defer GinkgoRecover()
err = mgr.Start(ctx)
Expect(err).NotTo(HaveOccurred())
}()
// wait for the webhook server to get ready.
dialer := &net.Dialer{Timeout: time.Second}
addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
Eventually(func() error {
conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
if err != nil {
return err
}
return conn.Close()
}).Should(Succeed())
})
var _ = AfterSuite(func() {
By("tearing down the test environment")
cancel()
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})
// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
basePath := filepath.Join("..", "..", "..", "bin", "k8s")
entries, err := os.ReadDir(basePath)
if err != nil {
logf.Log.Error(err, "Failed to read directory", "path", basePath)
return ""
}
for _, entry := range entries {
if entry.IsDir() {
return filepath.Join(basePath, entry.Name())
}
}
return ""
}