Skip to content

Commit

Permalink
chore: unittest for k8s pkg
Browse files Browse the repository at this point in the history
  • Loading branch information
mojtaba-esk committed Jun 4, 2024
1 parent 4e6dcf0 commit 8352b2f
Show file tree
Hide file tree
Showing 21 changed files with 3,651 additions and 82 deletions.
72 changes: 19 additions & 53 deletions pkg/k8s/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,12 @@ package k8s

import (
"context"
"os"
"path/filepath"
"regexp"
"strings"
"time"

"github.com/sirupsen/logrus"
"k8s.io/client-go/discovery"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)

const (
Expand All @@ -29,8 +23,8 @@ const (
)

type Client struct {
clientset *kubernetes.Clientset
discoveryClient *discovery.DiscoveryClient
clientset kubernetes.Interface
discoveryClient discovery.DiscoveryInterface
dynamicClient dynamic.Interface
namespace string
}
Expand Down Expand Up @@ -75,7 +69,21 @@ func New(ctx context.Context, namespace string) (*Client, error) {
return kc, nil
}

func (c *Client) Clientset() *kubernetes.Clientset {
func NewCustom(
cs kubernetes.Interface,
dc discovery.DiscoveryInterface,
dC dynamic.Interface,
namespace string,
) *Client {
return &Client{
clientset: cs,
discoveryClient: dc,
dynamicClient: dC,
namespace: namespace,
}
}

func (c *Client) Clientset() kubernetes.Interface {
return c.clientset
}

Expand All @@ -87,48 +95,6 @@ func (c *Client) Namespace() string {
return c.namespace
}

// isClusterEnvironment checks if the program is running in a Kubernetes cluster.
func isClusterEnvironment() bool {
return fileExists(tokenPath) && fileExists(certPath)
}

func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}

// getClusterConfig returns the appropriate Kubernetes cluster configuration.
func getClusterConfig() (*rest.Config, error) {
if isClusterEnvironment() {
return rest.InClusterConfig()
}

// build the configuration from the kubeconfig file
kubeconfig := filepath.Join(os.Getenv("HOME"), ".kube", "config")
return clientcmd.BuildConfigFromFlags("", kubeconfig)
}

// precompile the regular expression to avoid recompiling it on every function call
var invalidCharsRegexp = regexp.MustCompile(`[^a-z0-9-]+`)

// SanitizeName ensures compliance with Kubernetes DNS-1123 subdomain names. It:
// 1. Converts the input string to lowercase.
// 2. Replaces underscores and any non-DNS-1123 compliant characters with hyphens.
// 3. Trims leading and trailing hyphens.
// 4. Ensures the name does not exceed 63 characters, trimming excess characters if necessary
// and ensuring it does not end with a hyphen after trimming.
//
// Use this function to sanitize strings to be used as Kubernetes names for resources.
func SanitizeName(name string) string {
sanitized := strings.ToLower(name)
// Replace underscores and any other disallowed characters with hyphens
sanitized = invalidCharsRegexp.ReplaceAllString(sanitized, "-")
// Trim leading and trailing hyphens
sanitized = strings.Trim(sanitized, "-")
if len(sanitized) > 63 {
sanitized = sanitized[:63]
// Ensure it does not end with a hyphen after cutting it to the max length
sanitized = strings.TrimRight(sanitized, "-")
}
return sanitized
func (c *Client) DiscoveryClient() discovery.DiscoveryInterface {
return c.discoveryClient
}
271 changes: 271 additions & 0 deletions pkg/k8s/k8s_configmap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
package k8s_test

import (
"context"
"errors"

"github.com/celestiaorg/knuu/pkg/k8s"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/kubernetes/fake"
k8stesting "k8s.io/client-go/testing"
)

func (suite *TestSuite) TestGetConfigMap() {
tests := []struct {
name string
configMapName string
setupMock func(*fake.Clientset)
expectedErr error
expectedCM *v1.ConfigMap
}{
{
name: "successful retrieval",
configMapName: "test-configmap",
setupMock: func(clientset *fake.Clientset) {
err := createConfigMap(clientset, "test-configmap", suite.namespace)
require.NoError(suite.T(), err)
},
expectedErr: nil,
expectedCM: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "test-configmap",
Namespace: suite.namespace,
},
},
},
{
name: "configmap not found",
configMapName: "non-existent-configmap",
setupMock: func(clientset *fake.Clientset) {
// No setup needed for this case
},
expectedErr: k8s.ErrGettingConfigmap.WithParams("non-existent-configmap").
Wrap(errors.New("configmaps \"non-existent-configmap\" not found")),
expectedCM: nil,
},
{
name: "client error",
configMapName: "error-configmap",
setupMock: func(clientset *fake.Clientset) {
clientset.PrependReactor("get", "configmaps", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, errors.New("internal server error")
})
},
expectedErr: k8s.ErrGettingConfigmap.WithParams("error-configmap").
Wrap(errors.New("internal server error")),
expectedCM: nil,
},
}

for _, tt := range tests {
suite.Run(tt.name, func() {
tt.setupMock(suite.client.Clientset().(*fake.Clientset))

cm, err := suite.client.GetConfigMap(context.Background(), tt.configMapName)
if tt.expectedErr != nil {
require.Error(suite.T(), err)
assert.ErrorIs(suite.T(), err, tt.expectedErr)
return
}

require.NoError(suite.T(), err)
assert.EqualValues(suite.T(), tt.expectedCM, cm)
})
}
}

func (suite *TestSuite) TestConfigMapExists() {
tests := []struct {
name string
configMapName string
setupMock func(*fake.Clientset)
expectedExist bool
expectedErr error
}{
{
name: "configmap exists",
configMapName: "existing-configmap",
setupMock: func(clientset *fake.Clientset) {
err := createConfigMap(clientset, "existing-configmap", suite.namespace)
require.NoError(suite.T(), err)
},
expectedExist: true,
expectedErr: nil,
},
{
name: "configmap does not exist",
configMapName: "non-existent-configmap",
setupMock: func(clientset *fake.Clientset) {},
expectedExist: false,
expectedErr: nil,
},
{
name: "client error",
configMapName: "error-configmap",
setupMock: func(clientset *fake.Clientset) {
clientset.PrependReactor("get", "configmaps", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, errors.New("internal server error")
})
},
expectedExist: false,
expectedErr: k8s.ErrGettingConfigmap.WithParams("error-configmap").
Wrap(errors.New("internal server error")),
},
}

for _, tt := range tests {
suite.Run(tt.name, func() {
tt.setupMock(suite.client.Clientset().(*fake.Clientset))

exists, err := suite.client.ConfigMapExists(context.Background(), tt.configMapName)
if tt.expectedErr != nil {
require.Error(suite.T(), err)
assert.ErrorIs(suite.T(), err, tt.expectedErr)
return
}

require.NoError(suite.T(), err)
assert.Equal(suite.T(), tt.expectedExist, exists)
})
}
}

func (suite *TestSuite) TestCreateConfigMap() {
tests := []struct {
name string
configMap *v1.ConfigMap
setupMock func(*fake.Clientset)
expectedErr error
}{
{
name: "successful creation",
configMap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "new-configmap",
Namespace: suite.namespace,
},
Data: map[string]string{"key": "value"},
},
setupMock: func(clientset *fake.Clientset) {},
expectedErr: nil,
},
{
name: "configmap already exists",
configMap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "existing-configmap",
Namespace: suite.namespace,
},
},
setupMock: func(clientset *fake.Clientset) {
err := createConfigMap(clientset, "existing-configmap", suite.namespace)
require.NoError(suite.T(), err)
},
expectedErr: k8s.ErrConfigmapAlreadyExists.WithParams("existing-configmap").
Wrap(errors.New("configmap already exists")),
},
{
name: "client error",
configMap: &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "error-configmap",
Namespace: suite.namespace,
},
},
setupMock: func(clientset *fake.Clientset) {
clientset.PrependReactor("create", "configmaps", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, errors.New("internal server error")
})
},
expectedErr: k8s.ErrCreatingConfigmap.WithParams("error-configmap").
Wrap(errors.New("internal server error")),
},
}

for _, tt := range tests {
suite.Run(tt.name, func() {
tt.setupMock(suite.client.Clientset().(*fake.Clientset))

cm, err := suite.client.CreateConfigMap(context.Background(), tt.configMap.Name, tt.configMap.Labels, tt.configMap.Data)
if tt.expectedErr != nil {
require.Error(suite.T(), err)
assert.ErrorIs(suite.T(), err, tt.expectedErr)
return
}

require.NoError(suite.T(), err)
assert.EqualValues(suite.T(), tt.configMap, cm)
})
}
}

func (suite *TestSuite) TestDeleteConfigMap() {
tests := []struct {
name string
configMapName string
setupMock func(*fake.Clientset)
expectedErr error
}{
{
name: "successful deletion",
configMapName: "existing-configmap",
setupMock: func(clientset *fake.Clientset) {
err := createConfigMap(clientset, "existing-configmap", suite.namespace)
require.NoError(suite.T(), err)
},
expectedErr: nil,
},
{
name: "configmap does not exist",
configMapName: "non-existent-configmap",
setupMock: func(clientset *fake.Clientset) {},
expectedErr: k8s.ErrConfigmapDoesNotExist.WithParams("non-existent-configmap").
Wrap(errors.New("configmap does not exist")),
},
{
name: "client error",
configMapName: "error-configmap",
setupMock: func(clientset *fake.Clientset) {
// if it does not exist, it return nil as error
// so we need to add it to the fake client to be able to pass the existence check
err := createConfigMap(clientset, "error-configmap", suite.namespace)
require.NoError(suite.T(), err)

clientset.PrependReactor("delete", "configmaps", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, errors.New("internal server error")
})
},
expectedErr: k8s.ErrDeletingConfigmap.WithParams("error-configmap").
Wrap(errors.New("internal server error")),
},
}

for _, tt := range tests {
suite.Run(tt.name, func() {
tt.setupMock(suite.client.Clientset().(*fake.Clientset))

err := suite.client.DeleteConfigMap(context.Background(), tt.configMapName)
if tt.expectedErr != nil {
require.Error(suite.T(), err)
assert.ErrorIs(suite.T(), err, tt.expectedErr)
return
}

require.NoError(suite.T(), err)
})
}
}

func createConfigMap(clientset *fake.Clientset, name, namespace string) error {
_, err := clientset.CoreV1().ConfigMaps(namespace).Create(context.Background(), &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
}, metav1.CreateOptions{})
return err
}
2 changes: 1 addition & 1 deletion pkg/k8s/k8s_custom_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func (c *Client) CreateCustomResource(
},
}

if _, err := c.dynamicClient.Resource(*gvr).Namespace(c.namespace).Create(context.TODO(), resourceUnstructured, metav1.CreateOptions{}); err != nil {
if _, err := c.dynamicClient.Resource(*gvr).Namespace(c.namespace).Create(ctx, resourceUnstructured, metav1.CreateOptions{}); err != nil {
return ErrCreatingCustomResource.WithParams(gvr.Resource).Wrap(err)
}

Expand Down
Loading

0 comments on commit 8352b2f

Please sign in to comment.