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

chore: unittest for k8s pkg #416

Merged
merged 15 commits into from
Jun 14, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
21 changes: 10 additions & 11 deletions pkg/builder/kaniko/kaniko.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import (
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"

"github.com/celestiaorg/knuu/pkg/builder"
"github.com/celestiaorg/knuu/pkg/k8s"
"github.com/celestiaorg/knuu/pkg/minio"
"github.com/celestiaorg/knuu/pkg/names"
)
Expand All @@ -31,10 +31,9 @@ const (
)

type Kaniko struct {
K8sClientset kubernetes.Interface
K8sNamespace string
Minio *minio.Minio // Minio service to store the build context if it's a directory
ContentName string // Name of the content pushed to Minio
K8s k8s.KubeManager
mojtaba-esk marked this conversation as resolved.
Show resolved Hide resolved
Minio *minio.Minio // Minio service to store the build context if it's a directory
ContentName string // Name of the content pushed to Minio
}

var _ builder.Builder = &Kaniko{}
Expand All @@ -45,7 +44,7 @@ func (k *Kaniko) Build(ctx context.Context, b *builder.BuilderOptions) (logs str
return "", ErrPreparingJob.Wrap(err)
}

cJob, err := k.K8sClientset.BatchV1().Jobs(k.K8sNamespace).Create(ctx, job, metav1.CreateOptions{})
cJob, err := k.K8s.Clientset().BatchV1().Jobs(k.K8s.Namespace()).Create(ctx, job, metav1.CreateOptions{})
if err != nil {
return "", ErrCreatingJob.Wrap(err)
}
Expand Down Expand Up @@ -77,7 +76,7 @@ func (k *Kaniko) Build(ctx context.Context, b *builder.BuilderOptions) (logs str
}

func (k *Kaniko) waitForJobCompletion(ctx context.Context, job *batchv1.Job) (*batchv1.Job, error) {
watcher, err := k.K8sClientset.BatchV1().Jobs(k.K8sNamespace).Watch(ctx, metav1.ListOptions{
watcher, err := k.K8s.Clientset().BatchV1().Jobs(k.K8s.Namespace()).Watch(ctx, metav1.ListOptions{
FieldSelector: fmt.Sprintf("metadata.name=%s", job.Name),
})
if err != nil {
Expand Down Expand Up @@ -108,7 +107,7 @@ func (k *Kaniko) waitForJobCompletion(ctx context.Context, job *batchv1.Job) (*b
}

func (k *Kaniko) firstPodFromJob(ctx context.Context, job *batchv1.Job) (*v1.Pod, error) {
podList, err := k.K8sClientset.CoreV1().Pods(k.K8sNamespace).List(ctx, metav1.ListOptions{
podList, err := k.K8s.Clientset().CoreV1().Pods(k.K8s.Namespace()).List(ctx, metav1.ListOptions{
LabelSelector: fmt.Sprintf("job-name=%s", job.Name),
})
if err != nil {
Expand All @@ -131,7 +130,7 @@ func (k *Kaniko) containerLogs(ctx context.Context, pod *v1.Pod) (string, error)
Container: pod.Spec.Containers[0].Name,
}

req := k.K8sClientset.CoreV1().Pods(k.K8sNamespace).GetLogs(pod.Name, &logOptions)
req := k.K8s.Clientset().CoreV1().Pods(k.K8s.Namespace()).GetLogs(pod.Name, &logOptions)
logs, err := req.DoRaw(ctx)
if err != nil {
return "", err
Expand All @@ -141,7 +140,7 @@ func (k *Kaniko) containerLogs(ctx context.Context, pod *v1.Pod) (string, error)
}

func (k *Kaniko) cleanup(ctx context.Context, job *batchv1.Job) error {
err := k.K8sClientset.BatchV1().Jobs(k.K8sNamespace).
err := k.K8s.Clientset().BatchV1().Jobs(k.K8s.Namespace()).
Delete(ctx, job.Name, metav1.DeleteOptions{
PropagationPolicy: &[]metav1.DeletionPropagation{metav1.DeletePropagationBackground}[0],
})
Expand All @@ -150,7 +149,7 @@ func (k *Kaniko) cleanup(ctx context.Context, job *batchv1.Job) error {
}

// Delete the associated Pods
err = k.K8sClientset.CoreV1().Pods(k.K8sNamespace).
err = k.K8s.Clientset().CoreV1().Pods(k.K8s.Namespace()).
DeleteCollection(ctx, metav1.DeleteOptions{}, metav1.ListOptions{
LabelSelector: fmt.Sprintf("job-name=%s", job.Name),
})
Expand Down
46 changes: 17 additions & 29 deletions pkg/builder/kaniko/kaniko_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,21 @@ import (
"k8s.io/client-go/kubernetes/fake"

"github.com/celestiaorg/knuu/pkg/builder"
"github.com/celestiaorg/knuu/pkg/k8s"
)

const (
k8sNamespace = "test-namespace"
k8sNamespace = "test-namespace"
testImage = "test-image"
testDestination = "registry.example.com/test-image:latest"
)

func TestKanikoBuilder(t *testing.T) {
k8sCS := fake.NewSimpleClientset()
kb := &Kaniko{
MSevey marked this conversation as resolved.
Show resolved Hide resolved
K8sClientset: k8sCS,
K8sNamespace: k8sNamespace,
K8s: k8s.NewCustom(k8sCS, k8sCS.Discovery(), nil, k8sNamespace),
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
ctx := context.Background()

t.Run("BuildSuccess", func(t *testing.T) {
blCtx := "git://github.com/mojtaba-esk/sample-docker"
Expand All @@ -36,9 +37,9 @@ func TestKanikoBuilder(t *testing.T) {
require.NoError(t, err, "GetDefaultCacheOptions should succeed")

buildOptions := &builder.BuilderOptions{
ImageName: "test-image",
ImageName: testImage,
BuildContext: blCtx,
Destination: "registry.example.com/test-image:latest",
Destination: testDestination,
Args: []string{"--build-arg=value"},
Cache: cacheOpts,
}
Expand All @@ -54,7 +55,7 @@ func TestKanikoBuilder(t *testing.T) {
}()

// Simulate the successful completion of the Job after a short delay
time.Sleep(2 * time.Second)
time.Sleep(500 * time.Millisecond)
completeAllJobInFakeClientset(t, k8sCS, k8sNamespace)

wg.Wait()
Expand All @@ -63,40 +64,27 @@ func TestKanikoBuilder(t *testing.T) {
assert.NotEmpty(t, logs, "Build logs should not be empty")
})

t.Run("BuildFailure", func(t *testing.T) {
buildOptions := &builder.BuilderOptions{
ImageName: "test-image",
BuildContext: "invalid-context", // Simulate an invalid context
Destination: "registry.example.com/test-image:latest",
}

logs, err := kb.Build(ctx, buildOptions)

assert.Error(t, err, "Build should fail")
assert.Empty(t, logs, "Build logs should be empty")
})

t.Run("BuildWithContextCancellation", func(t *testing.T) {
buildOptions := &builder.BuilderOptions{
ImageName: "test-image",
ImageName: testImage,
BuildContext: "git://example.com/repo",
Destination: "registry.example.com/test-image:latest",
Destination: testDestination,
}

// Cancel the context to simulate cancellation during the build
ctx, cancel := context.WithCancel(ctx)
cancel()

logs, err := kb.Build(ctx, buildOptions)

assert.Error(t, err, "Build should fail due to context cancellation")
assert.Empty(t, logs, "Build logs should be empty")
assert.Error(t, err, "build should fail due to context cancellation")
assert.Empty(t, logs, "build logs should be empty")
})

}

func completeAllJobInFakeClientset(t *testing.T, clientset *fake.Clientset, namespace string) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
ctx := context.Background()

job, err := clientset.BatchV1().Jobs(namespace).List(ctx, metav1.ListOptions{})
assert.NoError(t, err)
Expand Down Expand Up @@ -125,8 +113,8 @@ func createPodFromJob(job *batchv1.Job) *v1.Pod {
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: "fake-container", // Adjust as needed
Image: "fake-image", // Adjust as needed
Name: "fake-container",
Image: "fake-image",
},
},
},
Expand Down
85 changes: 19 additions & 66 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 @@ -35,8 +29,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 @@ -81,7 +75,21 @@ func New(ctx context.Context, namespace string) (*Client, error) {
return kc, nil
}

func (c *Client) Clientset() *kubernetes.Clientset {
func NewCustom(
MSevey marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -93,61 +101,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.
// If the program is running in a Kubernetes cluster, it returns the in-cluster configuration.
// Otherwise, it returns the configuration from the kubeconfig file.
//
// The QPS and Burst settings are increased to allow for higher throughput and concurrency.
func getClusterConfig() (config *rest.Config, err error) {
if isClusterEnvironment() {
config, err = rest.InClusterConfig()
} else {
// build the configuration from the kubeconfig file
kubeconfig := filepath.Join(os.Getenv("HOME"), ".kube", "config")
config, err = clientcmd.BuildConfigFromFlags("", kubeconfig)
}
if err != nil {
logrus.Errorf("Error getting kubernetes config: %v", err)
return nil, err
}

// Increase QPS and Burst settings
config.QPS = CustomQPS
config.Burst = CustomBurst
return config, nil
}

// 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
}
Loading
Loading