@@ -22,12 +22,18 @@ package reconcile
2222
2323import (
2424 "context"
25+ "fmt"
2526 "strings"
2627
28+ "github.com/rs/zerolog"
29+ core "k8s.io/api/core/v1"
30+ "k8s.io/apimachinery/pkg/api/resource"
31+
32+ "github.com/arangodb/kube-arangodb/pkg/apis/deployment"
2733 api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1"
34+ "github.com/arangodb/kube-arangodb/pkg/util"
2835 "github.com/arangodb/kube-arangodb/pkg/util/k8sutil"
2936 inspectorInterface "github.com/arangodb/kube-arangodb/pkg/util/k8sutil/inspector"
30- "github.com/rs/zerolog"
3137)
3238
3339func createMemberRecreationConditionsPlan (ctx context.Context ,
@@ -37,17 +43,18 @@ func createMemberRecreationConditionsPlan(ctx context.Context,
3743 var p api.Plan
3844
3945 for _ , m := range status .Members .AsList () {
40- resp , recreate := EvaluateMemberRecreationCondition (ctx , log , apiObject , spec , status , m .Group , m .Member , cachedStatus , context )
46+ message , recreate := EvaluateMemberRecreationCondition (ctx , log , apiObject , spec , status , m .Group , m .Member ,
47+ cachedStatus , context , isStorageClassChanged , isVolumeSizeChanged )
4148
4249 if ! recreate {
4350 if _ , ok := m .Member .Conditions .Get (api .MemberReplacementRequired ); ok {
4451 // Unset condition
4552 p = append (p , removeMemberConditionActionV2 ("Member replacement not required" , api .MemberReplacementRequired , m .Group , m .Member .ID ))
4653 }
4754 } else {
48- if c , ok := m .Member .Conditions .Get (api .MemberReplacementRequired ); ! ok || ! c .IsTrue () || c .Message != resp {
55+ if c , ok := m .Member .Conditions .Get (api .MemberReplacementRequired ); ! ok || ! c .IsTrue () || c .Message != message {
4956 // Update condition
50- p = append (p , updateMemberConditionActionV2 ("Member replacement required" , api .MemberReplacementRequired , m .Group , m .Member .ID , true , "Member replacement required" , resp , "" ))
57+ p = append (p , updateMemberConditionActionV2 ("Member replacement required" , api .MemberReplacementRequired , m .Group , m .Member .ID , true , "Member replacement required" , message , "" ))
5158 }
5259 }
5360 }
@@ -59,7 +66,7 @@ type MemberRecreationConditionEvaluator func(ctx context.Context,
5966 log zerolog.Logger , apiObject k8sutil.APIObject ,
6067 spec api.DeploymentSpec , status api.DeploymentStatus ,
6168 group api.ServerGroup , member api.MemberStatus ,
62- cachedStatus inspectorInterface.Inspector , context PlanBuilderContext ) (string , bool )
69+ cachedStatus inspectorInterface.Inspector , context PlanBuilderContext ) (bool , string , error )
6370
6471func EvaluateMemberRecreationCondition (ctx context.Context ,
6572 log zerolog.Logger , apiObject k8sutil.APIObject ,
@@ -69,10 +76,170 @@ func EvaluateMemberRecreationCondition(ctx context.Context,
6976 args := make ([]string , 0 , len (evaluators ))
7077
7178 for _ , e := range evaluators {
72- if s , ok := e (ctx , log , apiObject , spec , status , group , member , cachedStatus , context ); ok {
79+ ok , s , err := e (ctx , log , apiObject , spec , status , group , member , cachedStatus , context )
80+ if err != nil {
81+ // When one of an evaluator requires pod's replacement then it should be done.
82+ continue
83+ }
84+
85+ if ok {
7386 args = append (args , s )
7487 }
7588 }
7689
7790 return strings .Join (args , ", " ), len (args ) > 0
7891}
92+
93+ // isStorageClassChanged returns true and reason when the member should be replaced.
94+ func isStorageClassChanged (_ context.Context , log zerolog.Logger , apiObject k8sutil.APIObject , spec api.DeploymentSpec ,
95+ _ api.DeploymentStatus , group api.ServerGroup , member api.MemberStatus ,
96+ cachedStatus inspectorInterface.Inspector , context PlanBuilderContext ) (bool , string , error ) {
97+ if spec .GetMode () == api .DeploymentModeSingle {
98+ // Storage cannot be changed in single server deployments.
99+ return false , "" , nil
100+ }
101+
102+ if member .Phase != api .MemberPhaseCreated {
103+ // Only make changes when phase is created.
104+ return false , "" , nil
105+ }
106+
107+ if member .PersistentVolumeClaimName == "" {
108+ // Plan is irrelevant without PVC.
109+ return false , "" , nil
110+ }
111+
112+ groupSpec := spec .GetServerGroupSpec (group )
113+ storageClassName := groupSpec .GetStorageClassName ()
114+ if storageClassName == "" {
115+ // A storage class is not set.
116+ return false , "" , nil
117+ }
118+
119+ // Check if a storage class changed.
120+ if pvc , ok := cachedStatus .PersistentVolumeClaim (member .PersistentVolumeClaimName ); ! ok {
121+ log .Warn ().Str ("role" , group .AsRole ()).Str ("id" , member .ID ).Msg ("Failed to get PVC" )
122+ return false , "" , fmt .Errorf ("failed to get PVC %s" , member .PersistentVolumeClaimName )
123+ } else {
124+ pvcClassName := util .StringOrDefault (pvc .Spec .StorageClassName )
125+ if pvcClassName == storageClassName {
126+ // A storage class has not been changed.
127+ return false , "" , nil
128+ }
129+ if pvcClassName == "" {
130+ // TODO what to do here?
131+ return false , "" , nil
132+ }
133+ }
134+
135+ // From here on, it is known that a storage class has changed.
136+ if group != api .ServerGroupDBServers && group != api .ServerGroupAgents {
137+ // Only agents & DB servers are allowed to change their storage class.
138+ context .CreateEvent (k8sutil .NewCannotChangeStorageClassEvent (apiObject , member .ID , group .AsRole (), "Not supported" ))
139+ return false , "" , nil
140+ }
141+
142+ // From here on it is known that the member requires replacement, so `true` must be returned.
143+ // If pod does not exist then it will try next time.
144+ if pod , ok := cachedStatus .Pod (member .PodName ); ok {
145+ if _ , ok := pod .GetAnnotations ()[deployment .ArangoDeploymentPodReplaceAnnotation ]; ! ok {
146+ log .Warn ().
147+ Str ("pod-name" , member .PodName ).
148+ Str ("server-group" , group .AsRole ()).
149+ Msgf ("try changing a storage class name, but %s" , getRequiredReplaceMessage (member .PodName ))
150+ // No return here.
151+ }
152+ } else {
153+ return false , "" , fmt .Errorf ("failed to get pod %s" , member .PodName )
154+ }
155+
156+ return true , "Storage class has changed" , nil
157+ }
158+
159+ // isVolumeSizeChanged returns true and reason when the member should be replaced.
160+ func isVolumeSizeChanged (_ context.Context , log zerolog.Logger , _ k8sutil.APIObject , spec api.DeploymentSpec ,
161+ _ api.DeploymentStatus , group api.ServerGroup , member api.MemberStatus ,
162+ cachedStatus inspectorInterface.Inspector , _ PlanBuilderContext ) (bool , string , error ) {
163+ if spec .GetMode () == api .DeploymentModeSingle {
164+ // Storage cannot be changed in single server deployments.
165+ return false , "" , nil
166+ }
167+
168+ if member .Phase != api .MemberPhaseCreated {
169+ // Only make changes when phase is created.
170+ return false , "" , nil
171+ }
172+
173+ if member .PersistentVolumeClaimName == "" {
174+ // Plan is irrelevant without PVC.
175+ return false , "" , nil
176+ }
177+
178+ pvc , ok := cachedStatus .PersistentVolumeClaim (member .PersistentVolumeClaimName )
179+ if ! ok {
180+ log .Warn ().
181+ Str ("role" , group .AsRole ()).
182+ Str ("id" , member .ID ).
183+ Msg ("Failed to get PVC" )
184+
185+ return false , "" , fmt .Errorf ("failed to get PVC %s" , member .PersistentVolumeClaimName )
186+ }
187+
188+ groupSpec := spec .GetServerGroupSpec (group )
189+ ok , volumeSize , requestedSize := shouldVolumeResize (groupSpec , pvc )
190+ if ! ok {
191+ return false , "" , nil
192+ }
193+
194+ if group != api .ServerGroupDBServers {
195+ log .Error ().
196+ Str ("pvc-storage-size" , volumeSize .String ()).
197+ Str ("requested-size" , requestedSize .String ()).
198+ Msgf ("Volume size should not shrink, because it is not possible for \" %s\" " , group .AsRole ())
199+
200+ return false , "" , nil
201+ }
202+
203+ // From here on it is known that the member requires replacement, so `true` must be returned.
204+ // If pod does not exist then it will try next time.
205+ if pod , ok := cachedStatus .Pod (member .PodName ); ok {
206+ if _ , ok := pod .GetAnnotations ()[deployment .ArangoDeploymentPodReplaceAnnotation ]; ! ok {
207+ log .Warn ().Str ("pod-name" , member .PodName ).
208+ Msgf ("try shrinking volume size, but %s" , getRequiredReplaceMessage (member .PodName ))
209+ // No return here.
210+ }
211+ } else {
212+ return false , "" , fmt .Errorf ("failed to get pod %s" , member .PodName )
213+ }
214+
215+ return true , "Volume is shrunk" , nil
216+ }
217+
218+ // shouldVolumeResize returns false when a volume should not resize.
219+ // Currently, it is only possible to shrink a volume size.
220+ // When return true then the actual and required volume size are returned.
221+ func shouldVolumeResize (groupSpec api.ServerGroupSpec ,
222+ pvc * core.PersistentVolumeClaim ) (bool , resource.Quantity , resource.Quantity ) {
223+ var res core.ResourceList
224+ if groupSpec .HasVolumeClaimTemplate () {
225+ res = groupSpec .GetVolumeClaimTemplate ().Spec .Resources .Requests
226+ } else {
227+ res = groupSpec .Resources .Requests
228+ }
229+
230+ if requestedSize , ok := res [core .ResourceStorage ]; ok {
231+ if volumeSize , ok := pvc .Spec .Resources .Requests [core .ResourceStorage ]; ok {
232+ if volumeSize .Cmp (requestedSize ) > 0 {
233+ // The actual PVC's volume size is greater than requested size, so it can be shrunk to the requested size.
234+ return true , volumeSize , requestedSize
235+ }
236+ }
237+ }
238+
239+ return false , resource.Quantity {}, resource.Quantity {}
240+ }
241+
242+ func getRequiredReplaceMessage (podName string ) string {
243+ return fmt .Sprintf ("%s annotation is required to be set on the pod %s" ,
244+ deployment .ArangoDeploymentPodReplaceAnnotation , podName )
245+ }
0 commit comments