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

Add hook package for handling Kubernetes node draining event #40

Merged
merged 14 commits into from
Jan 15, 2025
2 changes: 1 addition & 1 deletion e2e/preexiting_volume_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func Test_ExistingCivoVolume(t *testing.T) {
Spec: corev1.PersistentVolumeClaimSpec{
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
VolumeName: pv.Name,
Resources: corev1.ResourceRequirements{
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{
"storage": resource.MustParse("10Gi"),
},
Expand Down
2 changes: 1 addition & 1 deletion e2e/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func pvcSpec() *corev1.PersistentVolumeClaim {
Spec: corev1.PersistentVolumeClaimSpec{
StorageClassName: &storageClassName,
AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce},
Resources: corev1.ResourceRequirements{
Resources: corev1.VolumeResourceRequirements{
Requests: corev1.ResourceList{
"storage": resource.MustParse("10Gi"),
},
Expand Down
34 changes: 17 additions & 17 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,33 @@ require (
github.com/golang/protobuf v1.5.4
github.com/joho/godotenv v1.4.0
github.com/kubernetes-csi/csi-test/v4 v4.4.0
github.com/onsi/gomega v1.27.4
github.com/onsi/gomega v1.29.0
github.com/rs/zerolog v1.20.0
github.com/stretchr/testify v1.8.3
github.com/stretchr/testify v1.8.4
golang.org/x/sync v0.1.0
golang.org/x/sys v0.18.0
google.golang.org/grpc v1.56.3
k8s.io/api v0.27.1
k8s.io/apimachinery v0.27.1
k8s.io/client-go v0.24.2
k8s.io/api v0.29.0
k8s.io/apimachinery v0.29.0
k8s.io/client-go v0.29.0
k8s.io/mount-utils v0.24.3
sigs.k8s.io/controller-runtime v0.12.3
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.1 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/josharian/intern v1.0.0 // indirect
Expand All @@ -51,21 +51,21 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/oauth2 v0.7.0 // indirect
golang.org/x/oauth2 v0.10.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.90.1 // indirect
k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect
k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect
k8s.io/klog/v2 v2.110.1 // indirect
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
466 changes: 40 additions & 426 deletions go.sum

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions pkg/driver/hook/hook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package hook

import (
"context"
"errors"
"fmt"

"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)

// Hook defines the lifecycle methods for a hook, such as PreStop, PostStart, etc.
// Implementations of this interface can define actions to be performed at different lifecycle stages.
type Hook interface {
PreStop(ctx context.Context) error
}

type hook struct {
client kubernetes.Interface
nodeName string
clientCfgPath string
}

// NewHook creates a new Hook with the provided options. It returns an error if setup fails.
func NewHook(opts ...Option) (Hook, error) {
h := &hook{}
for _, opt := range append(defaultOpts, opts...) {
opt(h)
}
if h.nodeName == "" {
return nil, errors.New("node name not found")
}
if err := h.setupKubernetesClient(); err != nil {
return nil, fmt.Errorf("failed to setup kubernetes API client: %w", err)
}
return h, nil
}

// setupKubernetesClient creates Kubernetes client based on the kubeconfig path.
// If kubeconfig path is not empty, the client will be created using that path.
// Otherwise, if the kubeconfig path is empty, the client will be created using the in-clustetr config.
func (h *hook) setupKubernetesClient() (err error) {
if h.clientCfgPath != "" && h.client == nil {
cfg, err := clientcmd.BuildConfigFromFlags("", h.clientCfgPath)
if err != nil {
return fmt.Errorf("failed to build kubeconfig from path %q: %w", h.clientCfgPath, err)
}
h.client, err = kubernetes.NewForConfig(cfg)
if err != nil {
return fmt.Errorf("failed to create kubernetes API client: %w", err)
}
return nil
}

if h.client == nil {
cfg, err := rest.InClusterConfig()
if err != nil {
return fmt.Errorf("failed to load in-cluster kubeconfig: %w", err)
}
h.client, err = kubernetes.NewForConfig(cfg)
if err != nil {
return fmt.Errorf("failed to create kubernetes API client: %w", err)
}
}
return nil
}
41 changes: 41 additions & 0 deletions pkg/driver/hook/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package hook

import (
"os"

"k8s.io/client-go/kubernetes"
)

// Option represents a configuration function that modifies hook object.
type Option func(*hook)

var defaultOpts = []Option{
WithNodeName(os.Getenv("KUBE_NODE_NAME")),
}

// WithKubernetesClient returns Option to set Kubernetes API client.
func WithKubernetesClient(client kubernetes.Interface) Option {
return func(h *hook) {
if client != nil {
h.client = client
}
}
}

// WithKubernetesClient returns Option to set Kubernetes config path.
func WithKubernetesClientConfigPath(path string) Option {
return func(h *hook) {
if path != "" {
h.clientCfgPath = path
}
}
}

// WithNodeName returns Option to set node name.
func WithNodeName(name string) Option {
return func(h *hook) {
if name != "" {
h.nodeName = name
}
}
}
122 changes: 122 additions & 0 deletions pkg/driver/hook/options_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package hook

import (
"testing"

"github.com/stretchr/testify/assert"
"k8s.io/client-go/kubernetes"
)

func TestWithKubernetesClient(t *testing.T) {
type test struct {
name string
client kubernetes.Interface
beforeFunc func(*hook)
wantClient kubernetes.Interface
}

tests := []test{
{
name: "Succeeds to apply option",
client: &kubernetes.Clientset{},
wantClient: &kubernetes.Clientset{},
},
{
name: "Does nothing when client is nil",
beforeFunc: func(h *hook) {
h.client = &kubernetes.Clientset{}
},
wantClient: &kubernetes.Clientset{},
},
}

for _, test := range tests {
t.Run(test.name, func(tt *testing.T) {
h := &hook{}

if test.beforeFunc != nil {
test.beforeFunc(h)
}

WithKubernetesClient(test.client)(h)

assert.Equal(tt, test.wantClient, h.client)
})
}
}

func TestWithKubernetesClientConfigPath(t *testing.T) {
type test struct {
name string
path string
beforeFunc func(*hook)
wantPath string
}

tests := []test{
{
name: "Succeeds to apply option",
path: "kubeconfig.yaml",
wantPath: "kubeconfig.yaml",
},
{
name: "Do nothing when path is empty",
beforeFunc: func(h *hook) {
h.clientCfgPath = "kubeconfig.yaml"
},
wantPath: "kubeconfig.yaml",
},
}

for _, test := range tests {
t.Run(test.name, func(tt *testing.T) {
h := &hook{}

if test.beforeFunc != nil {
test.beforeFunc(h)
}

WithKubernetesClientConfigPath(test.path)(h)

assert.Equal(tt, test.wantPath, h.clientCfgPath)
})
}
}

func TestWithNodeName(t *testing.T) {
type test struct {
name string
nodeName string
beforeFunc func(*hook)
wantNodeName string
}

tests := []test{
{
name: "Succeeds to apply option",
nodeName: "node-01",
wantNodeName: "node-01",
},
{
name: "Do nothing when Node name is empty",
beforeFunc: func(h *hook) {
h.nodeName = "node-01"
},
wantNodeName: "node-01",
},
}

for _, test := range tests {
t.Run(test.name, func(tt *testing.T) {
h := &hook{}

if test.beforeFunc != nil {
test.beforeFunc(h)
}

WithNodeName(test.nodeName)(h)

assert.Equal(tt, test.wantNodeName, h.nodeName)
})
}
}
Loading
Loading