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

Restructure Project and Add Unit Tests #176

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
71 changes: 71 additions & 0 deletions cmd/rename-pvc/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package main

import (
"bytes"
"context"
"github.com/spf13/cobra"
"io"
"k8s.io/cli-runtime/pkg/genericclioptions"
"testing"
"time"
)

type mockIOStreams struct {
In io.Reader
Out *bytes.Buffer
ErrOut *bytes.Buffer
}

func mockCommand(streams genericclioptions.IOStreams) *cobra.Command {
return &cobra.Command{
Use: "mock-rename-pvc",
RunE: func(cmd *cobra.Command, args []string) error {
// Simulate some work
time.Sleep(100 * time.Millisecond)
_, err := io.WriteString(streams.Out, "Mock command executed")
return err
},
}
}

func TestRun(t *testing.T) {
mockStreams := &mockIOStreams{
In: &bytes.Buffer{},
Out: &bytes.Buffer{},
ErrOut: &bytes.Buffer{},
}

mockRun := func() error {
cmd := mockCommand(genericclioptions.IOStreams{
In: mockStreams.In,
Out: mockStreams.Out,
ErrOut: mockStreams.ErrOut,
})
return cmd.Execute()
}

_, cancel := context.WithCancel(context.Background())
defer cancel()

errCh := make(chan error, 1)
go func() {
errCh <- mockRun()
}()

time.Sleep(50 * time.Millisecond)

cancel()

select {
case err := <-errCh:
if err != nil && err != context.Canceled {
t.Errorf("run() returned an unexpected error: %v", err)
}
case <-time.After(1 * time.Second):
t.Error("run() did not complete within the expected time")
}

if !bytes.Contains(mockStreams.Out.Bytes(), []byte("Mock command executed")) {
t.Error("Expected output not found")
}
}
110 changes: 110 additions & 0 deletions pkg/renamepvc/execution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package renamepvc

import (
"context"
"fmt"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"time"
)

func (o *renamePVCOptions) execute(ctx context.Context) error {
oldPvc, err := o.k8sClient.CoreV1().PersistentVolumeClaims(o.sourceNamespace).Get(ctx, o.oldName, metav1.GetOptions{})
if err != nil {
return err
}

if err := o.checkIfMounted(ctx, oldPvc); err != nil {
return err
}

return o.rename(ctx, oldPvc)
}

// rename renames the PVC.
func (o *renamePVCOptions) rename(ctx context.Context, oldPvc *corev1.PersistentVolumeClaim) error {
newPvc, err := o.createNewPVC(ctx, oldPvc)
if err != nil {
return fmt.Errorf("failed to create new PVC: %w", err)
}

pv, err := o.updatePVClaimRef(ctx, oldPvc, newPvc)
if err != nil {
return fmt.Errorf("failed to update PV claim ref: %w", err)
}

if err := o.waitUntilPvcIsBound(ctx, newPvc); err != nil {
return fmt.Errorf("failed to wait for PVC to be bound: %w", err)
}

if err := o.deleteOldPVC(ctx, oldPvc); err != nil {
return fmt.Errorf("failed to delete old PVC: %w", err)
}

o.printSuccess(newPvc, pv)
return nil
}

// waitUntilPvcIsBound waits until the new PVC is bound.
func (o *renamePVCOptions) waitUntilPvcIsBound(ctx context.Context, pvc *corev1.PersistentVolumeClaim) error {
for i := 0; i < 60; i++ {
checkPVC, err := o.k8sClient.CoreV1().PersistentVolumeClaims(pvc.Namespace).Get(ctx, pvc.GetName(), metav1.GetOptions{})
if err != nil {
return err
}

if checkPVC.Status.Phase == corev1.ClaimBound {
return nil
}

select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Second):
}
}

return ErrNotBound
}

// createNewPVC creates a new PVC with the same spec as the old PVC but with a new name and namespace.
func (o *renamePVCOptions) createNewPVC(ctx context.Context, oldPvc *corev1.PersistentVolumeClaim) (*corev1.PersistentVolumeClaim, error) {
newPvc := createNewPVC(oldPvc, o.newName, o.targetNamespace)
newPvc, err := o.k8sClient.CoreV1().PersistentVolumeClaims(o.targetNamespace).Create(ctx, newPvc, metav1.CreateOptions{})
if err != nil {
return nil, err
}
fmt.Fprintf(o.streams.Out, "New PVC with name '%s' created\n", newPvc.Name)
return newPvc, nil
}

// updatePVClaimRef updates the ClaimRef of the PV to the new PVC.
func (o *renamePVCOptions) updatePVClaimRef(ctx context.Context, oldPvc, newPvc *corev1.PersistentVolumeClaim) (*corev1.PersistentVolume, error) {
pv, err := o.k8sClient.CoreV1().PersistentVolumes().Get(ctx, oldPvc.Spec.VolumeName, metav1.GetOptions{})
if err != nil {
return nil, err
}

pv.Spec.ClaimRef = createClaimRef(newPvc)
pv, err = o.k8sClient.CoreV1().PersistentVolumes().Update(ctx, pv, metav1.UpdateOptions{})
if err != nil {
return nil, err
}
fmt.Fprintf(o.streams.Out, "ClaimRef of PV '%s' is updated to new PVC '%s'\n", pv.Name, newPvc.Name)
return pv, nil
}

// deleteOldPVC deletes the old PVC.
func (o *renamePVCOptions) deleteOldPVC(ctx context.Context, oldPvc *corev1.PersistentVolumeClaim) error {
err := o.k8sClient.CoreV1().PersistentVolumeClaims(o.sourceNamespace).Delete(ctx, oldPvc.Name, metav1.DeleteOptions{})
if err != nil {
return err
}
fmt.Fprintf(o.streams.Out, "Old PVC '%s' is deleted\n", oldPvc.Name)
return nil
}

// printSuccess prints a success message to the output stream.
func (o *renamePVCOptions) printSuccess(newPvc *corev1.PersistentVolumeClaim, pv *corev1.PersistentVolume) {
fmt.Fprintf(o.streams.Out, "New PVC '%s' is bound to PV '%s'\n", newPvc.Name, pv.Name)
}
39 changes: 39 additions & 0 deletions pkg/renamepvc/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package renamepvc

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/kubernetes"
)

func getK8sClient(configFlags *genericclioptions.ConfigFlags) (*kubernetes.Clientset, error) {
config, err := configFlags.ToRESTConfig()
if err != nil {
return nil, err
}
return kubernetes.NewForConfig(config)
}

func createNewPVC(oldPvc *corev1.PersistentVolumeClaim, newName, targetNamespace string) *corev1.PersistentVolumeClaim {
newPvc := oldPvc.DeepCopy()
newPvc.Status = corev1.PersistentVolumeClaimStatus{}
newPvc.Name = newName
newPvc.UID = ""
newPvc.CreationTimestamp = metav1.Now()
newPvc.SelfLink = ""
newPvc.ResourceVersion = ""
newPvc.Namespace = targetNamespace
return newPvc
}

func createClaimRef(pvc *corev1.PersistentVolumeClaim) *corev1.ObjectReference {
return &corev1.ObjectReference{
Kind: pvc.Kind,
Namespace: pvc.Namespace,
Name: pvc.Name,
UID: pvc.UID,
APIVersion: pvc.APIVersion,
ResourceVersion: pvc.ResourceVersion,
}
}
77 changes: 77 additions & 0 deletions pkg/renamepvc/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package renamepvc

import (
"bufio"
"fmt"
"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/kubernetes"
"strings"
)

type renamePVCOptions struct {
streams genericclioptions.IOStreams
configFlags *genericclioptions.ConfigFlags
k8sClient kubernetes.Interface

confirm bool
oldName string
newName string
sourceNamespace string
targetNamespace string
}

func (o *renamePVCOptions) complete(args []string) error {
var err error
o.oldName = args[0]
o.newName = args[1]

o.sourceNamespace, _, err = o.configFlags.ToRawKubeConfigLoader().Namespace()
if err != nil {
return err
}

if o.targetNamespace == "" {
o.targetNamespace = o.sourceNamespace
}

o.k8sClient, err = getK8sClient(o.configFlags)
return err
}

func (o *renamePVCOptions) validate() error {
if !o.confirm {
return o.confirmCheck()
}
return nil
}

func (o *renamePVCOptions) confirmCheck() error {
_, err := fmt.Fprintf(o.streams.Out,
"Rename PVC from '%s' in namespace '%s' to '%s' in namespace '%s'? (yes or no) ",
o.oldName, o.sourceNamespace, o.newName, o.targetNamespace)
if err != nil {
return err
}

input, err := bufio.NewReader(o.streams.In).ReadString('\n')
if err != nil {
return err
}

switch strings.TrimSpace(strings.ToLower(input)) {
case "y", "yes":
return nil
case "n", "no":
return ErrConfirmationNotSuccessful
default:
return ErrConfirmationUnknown
}
}

func (o *renamePVCOptions) addFlags(cmd *cobra.Command) {
cmd.Flags().BoolVarP(&o.confirm, "yes", "y", false, "Skips confirmation if flag is set")
cmd.Flags().StringVarP(&o.targetNamespace, "target-namespace", "N", "",
"Defines in which namespace the new PVC should be created. By default the source PVC's namespace is used.")
o.configFlags.AddFlags(cmd.Flags())
}
Loading