Skip to content

Commit

Permalink
keep same node if pod mounts exclusive access PVC (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
Telemaco019 authored Jun 18, 2024
1 parent ffe2a7c commit d38fd73
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 22 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
### New features

* Show prompt for selecting a Pod in the current namespace when no Pod name is provided as argument.
* Properly handle Pods that mount persistent storage.
Pods that mount a PersistentVolume with exclusive access modes (`ReadWriteOnce`, `ReadWriteOncePod`) are cloned
on the same node as the original. This ensures that the duplicate can also mount the same volume.

### Chores

Expand Down
21 changes: 21 additions & 0 deletions pkg/cmd/pod_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package cmd

import (
"fmt"
"github.com/stretchr/testify/assert"
"github.com/telemaco019/duplik8s/pkg/test"
"github.com/telemaco019/duplik8s/pkg/test/mocks"
Expand All @@ -33,3 +34,23 @@ func Test_NoPodsAvailable(t *testing.T) {
assert.NotEmpty(t, output)
assert.Error(t, err)
}

func Test_Success(t *testing.T) {
podClient := mocks.NewPodClient(
mocks.ListPodsResult{},
nil,
)
cmd := NewRootCmd(podClient)
_, err := test.ExecuteCommand(cmd, "pod", "pod-1")
assert.NoError(t, err)
}

func Test_DuplicateError(t *testing.T) {
podClient := mocks.NewPodClient(
mocks.ListPodsResult{},
fmt.Errorf("error"),
)
cmd := NewRootCmd(podClient)
_, err := test.ExecuteCommand(cmd, "pod", "pod-1")
assert.EqualError(t, err, "error")
}
26 changes: 7 additions & 19 deletions pkg/pods/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,24 +63,6 @@ type PodOverrideOptions struct {
StartupProbe *v1.Probe
}

func (o PodOverrideOptions) Apply(pod *v1.Pod) {
// Override command
if o.Command != nil {
for i := range pod.Spec.Containers {
pod.Spec.Containers[i].Command = o.Command
pod.Spec.Containers[i].Args = o.Args
pod.Spec.Containers[i].ReadinessProbe = o.ReadinessProbe
pod.Spec.Containers[i].LivenessProbe = o.LivenessProbe
pod.Spec.Containers[i].ReadinessProbe = o.ReadinessProbe
pod.Spec.Containers[i].StartupProbe = o.StartupProbe
}
}
// Remove assigned nod
pod.Spec.NodeName = ""
// Override restart policy
pod.Spec.RestartPolicy = v1.RestartPolicyNever
}

func (c *podClient) ListPods(namespace string) ([]string, error) {
pods, err := c.clientset.CoreV1().Pods(namespace).List(c.ctx, metav1.ListOptions{})
if err != nil {
Expand Down Expand Up @@ -121,7 +103,13 @@ func (c *podClient) DuplicatePod(podName string, namespace string, opts PodOverr
},
Spec: pod.Spec,
}
opts.Apply(&newPod)

// override the pod spec
configurator := NewPodConfigurator(c.clientset, opts)
err = configurator.OverrideSpec(c.ctx, &newPod)
if err != nil {
return err
}

// create the new pod
_, err = c.clientset.CoreV1().Pods(pod.Namespace).Create(c.ctx, &newPod, metav1.CreateOptions{})
Expand Down
94 changes: 94 additions & 0 deletions pkg/pods/configurator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* Copyright 2024 Michele Zanotti <[email protected]>
*
* 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 pods

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

type PodConfigurator struct {
clientset *kubernetes.Clientset
options PodOverrideOptions
}

func NewPodConfigurator(clientset *kubernetes.Clientset, options PodOverrideOptions) PodConfigurator {
return PodConfigurator{
clientset: clientset,
options: options,
}
}

func (c PodConfigurator) OverrideSpec(ctx context.Context, pod *v1.Pod) error {
// Override command
if c.options.Command != nil {
for i := range pod.Spec.Containers {
pod.Spec.Containers[i].Command = c.options.Command
pod.Spec.Containers[i].Args = c.options.Args
pod.Spec.Containers[i].ReadinessProbe = c.options.ReadinessProbe
pod.Spec.Containers[i].LivenessProbe = c.options.LivenessProbe
pod.Spec.Containers[i].ReadinessProbe = c.options.ReadinessProbe
pod.Spec.Containers[i].StartupProbe = c.options.StartupProbe
}
}

hasMountOncePvc, err := c.hasMountOncePvc(ctx, *pod)
if err != nil {
return err
}

// If the Pod does not have any PVC with mount once policy, then remove the node name
// to allow the scheduler to schedule the pod on any node
if !hasMountOncePvc {
pod.Spec.NodeName = ""
}

// Override restart policy
pod.Spec.RestartPolicy = v1.RestartPolicyNever

return nil
}

func (c PodConfigurator) hasMountOncePvc(ctx context.Context, pod v1.Pod) (bool, error) {
for _, volume := range pod.Spec.Volumes {
if volume.PersistentVolumeClaim != nil {
pvc, err := c.clientset.
CoreV1().
PersistentVolumeClaims(pod.Namespace).
Get(ctx, volume.PersistentVolumeClaim.ClaimName, metav1.GetOptions{})
if err != nil {
return false, err
}
return anyMountOnceAccessMode(pvc.Spec.AccessModes), nil
}
}
return false, nil
}

func anyMountOnceAccessMode(modes []v1.PersistentVolumeAccessMode) bool {
for _, mode := range modes {
if mode == v1.ReadWriteOnce {
return true
}
if mode == v1.ReadWriteOncePod {
return true
}
}
return false
}
6 changes: 3 additions & 3 deletions pkg/test/mocks/pod_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ package mocks
import "github.com/telemaco019/duplik8s/pkg/pods"

type ListPodsResult struct {
pods []string
err error
Pods []string
Err error
}

type PodClient struct {
Expand All @@ -39,7 +39,7 @@ func NewPodClient(
}

func (c *PodClient) ListPods(namespace string) ([]string, error) {
return c.ListPodsResult.pods, c.ListPodsResult.err
return c.ListPodsResult.Pods, c.ListPodsResult.Err
}

func (c *PodClient) DuplicatePod(podName string, namespace string, opts pods.PodOverrideOptions) error {
Expand Down

0 comments on commit d38fd73

Please sign in to comment.