Skip to content

Commit 785e7bb

Browse files
committed
Align annotation names, refactoring, additional tests
1 parent eba3ff3 commit 785e7bb

3 files changed

Lines changed: 320 additions & 24 deletions

File tree

internal/pkg/handler/pause_deployment.go

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,77 @@ package handler
22

33
import (
44
"context"
5+
"fmt"
56
"time"
67

78
"github.com/sirupsen/logrus"
89
"github.com/stakater/Reloader/internal/pkg/options"
910
"github.com/stakater/Reloader/pkg/kube"
1011
app "k8s.io/api/apps/v1"
12+
"k8s.io/apimachinery/pkg/api/meta"
1113
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/runtime"
1215
)
1316

17+
// IsPaused checks if a deployment is currently paused
18+
func IsPaused(deployment *app.Deployment) bool {
19+
return deployment.Spec.Paused
20+
}
21+
22+
// IsPausedByReloader checks if a deployment was paused by reloader
23+
func IsPausedByReloader(deployment *app.Deployment) bool {
24+
if !deployment.Spec.Paused {
25+
return false
26+
}
27+
28+
pausedAtAnnotationValue := deployment.Annotations[options.PauseDeploymentTimeAnnotation]
29+
return pausedAtAnnotationValue != ""
30+
}
31+
32+
// FindDeploymentByName locates a deployment by name from a list of api objects
33+
func FindDeploymentByName(deployments []runtime.Object, deploymentName string) (*app.Deployment, error) {
34+
for _, deployment := range deployments {
35+
accessor, err := meta.Accessor(deployment)
36+
if err != nil {
37+
return nil, fmt.Errorf("error getting accessor for item: %v", err)
38+
}
39+
if accessor.GetName() == deploymentName {
40+
deploymentObj, ok := deployment.(*app.Deployment)
41+
if !ok {
42+
return nil, fmt.Errorf("failed to cast to Deployment")
43+
}
44+
return deploymentObj, nil
45+
}
46+
}
47+
return nil, fmt.Errorf("deployment '%s' not found", deploymentName)
48+
}
49+
50+
// GetPauseStartTime returns when the deployment was paused by reloader, nil otherwise
51+
func GetPauseStartTime(deployment *app.Deployment) (*time.Time, error) {
52+
if !IsPausedByReloader(deployment) {
53+
return nil, nil
54+
}
55+
56+
pausedAtStr := deployment.Annotations[options.PauseDeploymentTimeAnnotation]
57+
parsedTime, err := time.Parse(time.RFC3339, pausedAtStr)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
return &parsedTime, nil
63+
}
64+
65+
// PauseDeployment pauses a deployment for a specified duration and creates a timer to resume it
66+
// after the specified duration
1467
func PauseDeployment(deployment *app.Deployment, clients kube.Clients, deploymentName, namespace, pauseIntervalValue string) error {
1568
pauseDuration, err := ParsePauseDuration(pauseIntervalValue)
1669
if err != nil {
1770
return err
1871
}
1972

20-
if !deployment.Spec.Paused {
73+
if !IsPaused(deployment) {
2174
deployment.Spec.Paused = true
22-
logrus.Infof("Pausing Deployment '%s' in namespace '%s' for %s seconds", deploymentName, namespace, pauseDuration)
75+
logrus.Infof("Pausing Deployment '%s' in namespace '%s' for %s", deploymentName, namespace, pauseDuration)
2376

2477
if deployment.Annotations == nil {
2578
deployment.Annotations = make(map[string]string)
@@ -33,27 +86,23 @@ func PauseDeployment(deployment *app.Deployment, clients kube.Clients, deploymen
3386
return nil
3487
}
3588

89+
// CreateResumeTimer creates a timer to resume the deployment after the specified duration
3690
func CreateResumeTimer(deployment *app.Deployment, clients kube.Clients, deploymentName, namespace string, pauseDuration time.Duration) {
3791
time.AfterFunc(pauseDuration, func() {
3892
ResumeDeployment(deploymentName, namespace, clients)
3993
})
4094
}
4195

96+
// ResumeDeployment resumes a deployment that has been paused by reloader
4297
func ResumeDeployment(deploymentName, namespace string, clients kube.Clients) {
4398
deployment, err := clients.KubernetesClient.AppsV1().Deployments(namespace).Get(context.TODO(), deploymentName, metav1.GetOptions{})
4499
if err != nil {
45100
logrus.Errorf("Failed to get deployment '%s' in namespace '%s': %v", deploymentName, namespace, err)
46101
return
47102
}
48103

49-
if !deployment.Spec.Paused {
50-
logrus.Infof("Deployment '%s' in namespace '%s' not paused. Skipping resume", deploymentName, namespace)
51-
return
52-
}
53-
54-
pausedAtAnnotationValue := deployment.Annotations[options.PauseDeploymentTimeAnnotation]
55-
if pausedAtAnnotationValue == "" {
56-
logrus.Infof("Deployment '%s' in namespace '%s' was not paused by Reloader. Skipping resume", deploymentName, namespace)
104+
if !IsPausedByReloader(deployment) {
105+
logrus.Infof("Deployment '%s' in namespace '%s' not paused by Reloader. Skipping resume", deploymentName, namespace)
57106
return
58107
}
59108

@@ -63,11 +112,13 @@ func ResumeDeployment(deploymentName, namespace string, clients kube.Clients) {
63112
_, err = clients.KubernetesClient.AppsV1().Deployments(namespace).Update(context.TODO(), deployment, metav1.UpdateOptions{})
64113
if err != nil {
65114
logrus.Errorf("Failed to resume deployment '%s' in namespace '%s': %v", deploymentName, namespace, err)
115+
return
66116
}
67117

68118
logrus.Infof("Successfully resumed deployment '%s' in namespace '%s'", deploymentName, namespace)
69119
}
70120

121+
// ParsePauseDuration parses the pause interval value and returns a time.Duration
71122
func ParsePauseDuration(pauseIntervalValue string) (time.Duration, error) {
72123
pauseDuration, err := time.ParseDuration(pauseIntervalValue)
73124
if err != nil {
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package handler
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stakater/Reloader/internal/pkg/options"
8+
"github.com/stretchr/testify/assert"
9+
appsv1 "k8s.io/api/apps/v1"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/apimachinery/pkg/runtime"
12+
)
13+
14+
func TestIsPaused(t *testing.T) {
15+
tests := []struct {
16+
name string
17+
deployment *appsv1.Deployment
18+
paused bool
19+
}{
20+
{
21+
name: "paused deployment",
22+
deployment: &appsv1.Deployment{
23+
Spec: appsv1.DeploymentSpec{
24+
Paused: true,
25+
},
26+
},
27+
paused: true,
28+
},
29+
{
30+
name: "unpaused deployment",
31+
deployment: &appsv1.Deployment{
32+
Spec: appsv1.DeploymentSpec{
33+
Paused: false,
34+
},
35+
},
36+
paused: false,
37+
},
38+
}
39+
40+
for _, test := range tests {
41+
t.Run(test.name, func(t *testing.T) {
42+
result := IsPaused(test.deployment)
43+
assert.Equal(t, test.paused, result)
44+
})
45+
}
46+
}
47+
48+
func TestIsPausedByReloader(t *testing.T) {
49+
tests := []struct {
50+
name string
51+
deployment *appsv1.Deployment
52+
pausedByReloader bool
53+
}{
54+
{
55+
name: "paused by reloader",
56+
deployment: &appsv1.Deployment{
57+
Spec: appsv1.DeploymentSpec{
58+
Paused: true,
59+
},
60+
ObjectMeta: metav1.ObjectMeta{
61+
Annotations: map[string]string{
62+
options.PauseDeploymentTimeAnnotation: time.Now().Format(time.RFC3339),
63+
},
64+
},
65+
},
66+
pausedByReloader: true,
67+
},
68+
{
69+
name: "not paused by reloader",
70+
deployment: &appsv1.Deployment{
71+
Spec: appsv1.DeploymentSpec{
72+
Paused: true,
73+
},
74+
ObjectMeta: metav1.ObjectMeta{
75+
Annotations: map[string]string{},
76+
},
77+
},
78+
pausedByReloader: false,
79+
},
80+
{
81+
name: "not paused",
82+
deployment: &appsv1.Deployment{
83+
Spec: appsv1.DeploymentSpec{
84+
Paused: false,
85+
},
86+
},
87+
pausedByReloader: false,
88+
},
89+
}
90+
91+
for _, test := range tests {
92+
t.Run(test.name, func(t *testing.T) {
93+
pausedByReloader := IsPausedByReloader(test.deployment)
94+
assert.Equal(t, test.pausedByReloader, pausedByReloader)
95+
})
96+
}
97+
}
98+
99+
func TestFindDeploymentByName(t *testing.T) {
100+
testDeployment := &appsv1.Deployment{
101+
ObjectMeta: metav1.ObjectMeta{
102+
Name: "test-deployment",
103+
},
104+
}
105+
106+
additionalDeployment1 := &appsv1.Deployment{
107+
ObjectMeta: metav1.ObjectMeta{
108+
Name: "non-matching-deployment-1",
109+
},
110+
}
111+
112+
additionalDeployment2 := &appsv1.Deployment{
113+
ObjectMeta: metav1.ObjectMeta{
114+
Name: "non-matching-deployment-2",
115+
},
116+
}
117+
118+
tests := []struct {
119+
name string
120+
deployments []runtime.Object
121+
deploymentName string
122+
expectedDeployment *appsv1.Deployment
123+
deploymentFound bool
124+
}{
125+
{
126+
name: "deployment found",
127+
deployments: []runtime.Object{
128+
additionalDeployment1,
129+
testDeployment,
130+
additionalDeployment2,
131+
},
132+
deploymentName: "test-deployment",
133+
expectedDeployment: testDeployment,
134+
deploymentFound: true,
135+
},
136+
{
137+
name: "deployment not found",
138+
deployments: []runtime.Object{
139+
additionalDeployment1,
140+
testDeployment,
141+
additionalDeployment2,
142+
},
143+
deploymentName: "non-existent-deployment",
144+
expectedDeployment: nil,
145+
deploymentFound: false,
146+
},
147+
}
148+
149+
for _, test := range tests {
150+
t.Run(test.name, func(t *testing.T) {
151+
foundDeployment, err := FindDeploymentByName(test.deployments, test.deploymentName)
152+
153+
if test.deploymentFound {
154+
assert.NoError(t, err)
155+
assert.Equal(t, test.expectedDeployment, foundDeployment)
156+
} else {
157+
assert.Error(t, err)
158+
assert.Nil(t, foundDeployment)
159+
}
160+
})
161+
}
162+
}
163+
164+
func TestGetPauseStartTime(t *testing.T) {
165+
now := time.Now()
166+
nowStr := now.Format(time.RFC3339)
167+
168+
tests := []struct {
169+
name string
170+
deployment *appsv1.Deployment
171+
pausedByReloader bool
172+
expectedStartTime time.Time
173+
}{
174+
{
175+
name: "valid pause time",
176+
deployment: &appsv1.Deployment{
177+
Spec: appsv1.DeploymentSpec{
178+
Paused: true,
179+
},
180+
ObjectMeta: metav1.ObjectMeta{
181+
Annotations: map[string]string{
182+
options.PauseDeploymentTimeAnnotation: nowStr,
183+
},
184+
},
185+
},
186+
pausedByReloader: true,
187+
expectedStartTime: now,
188+
},
189+
{
190+
name: "not paused by reloader",
191+
deployment: &appsv1.Deployment{
192+
Spec: appsv1.DeploymentSpec{
193+
Paused: false,
194+
},
195+
},
196+
pausedByReloader: false,
197+
},
198+
}
199+
200+
for _, test := range tests {
201+
t.Run(test.name, func(t *testing.T) {
202+
actualStartTime, err := GetPauseStartTime(test.deployment)
203+
204+
assert.NoError(t, err)
205+
206+
if !test.pausedByReloader {
207+
assert.Nil(t, actualStartTime)
208+
} else {
209+
assert.NotNil(t, actualStartTime)
210+
assert.WithinDuration(t, test.expectedStartTime, *actualStartTime, time.Second)
211+
}
212+
})
213+
}
214+
}
215+
216+
func TestParsePauseDuration(t *testing.T) {
217+
tests := []struct {
218+
name string
219+
pauseIntervalValue string
220+
expectedDuration time.Duration
221+
invalidDuration bool
222+
}{
223+
{
224+
name: "valid duration",
225+
pauseIntervalValue: "10s",
226+
expectedDuration: 10 * time.Second,
227+
invalidDuration: false,
228+
},
229+
{
230+
name: "valid minute duration",
231+
pauseIntervalValue: "2m",
232+
expectedDuration: 2 * time.Minute,
233+
invalidDuration: false,
234+
},
235+
{
236+
name: "invalid duration",
237+
pauseIntervalValue: "invalid",
238+
expectedDuration: 0,
239+
invalidDuration: true,
240+
},
241+
}
242+
243+
for _, test := range tests {
244+
t.Run(test.name, func(t *testing.T) {
245+
actualDuration, err := ParsePauseDuration(test.pauseIntervalValue)
246+
247+
if test.invalidDuration {
248+
assert.Error(t, err)
249+
} else {
250+
assert.NoError(t, err)
251+
assert.Equal(t, test.expectedDuration, actualDuration)
252+
}
253+
})
254+
}
255+
}

0 commit comments

Comments
 (0)