statemanager.go 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. /*
  2. Licensed under the Apache License, Version 2.0 (the "License");
  3. you may not use this file except in compliance with the License.
  4. You may obtain a copy of the License at
  5. http://www.apache.org/licenses/LICENSE-2.0
  6. Unless required by applicable law or agreed to in writing, software
  7. distributed under the License is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. See the License for the specific language governing permissions and
  10. limitations under the License.
  11. */
  12. package statemanager
  13. import (
  14. "context"
  15. "errors"
  16. "fmt"
  17. "github.com/google/uuid"
  18. apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  19. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  20. "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
  21. "k8s.io/apimachinery/pkg/runtime"
  22. "sigs.k8s.io/controller-runtime/pkg/client"
  23. "github.com/external-secrets/external-secrets/apis/externalsecrets/v1beta1"
  24. "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
  25. "github.com/external-secrets/external-secrets/pkg/generator/gc"
  26. "github.com/external-secrets/external-secrets/pkg/utils/resolvers"
  27. )
  28. // Manager takes care of maintaining the state of the generators.
  29. // It provides the ability to commit and rollback the state of the generators,
  30. // which is needed when we have multiple generators that need to be created or
  31. // other operations which can fail.
  32. // The manager shall be used to modify the state of the generators on a given resource.
  33. // The user can choose any key to store the state of the generator on the "latest" field.
  34. // When state is moved to GC, manager will create a hash of the key and the generator state
  35. // and store it in the "GC" field.
  36. type Manager struct {
  37. scheme *runtime.Scheme
  38. client client.Client
  39. namespace string
  40. resource v1beta1.GeneratorStateManagingResource
  41. internalState []QueueItem
  42. }
  43. type QueueItem struct {
  44. Rollback func() error
  45. Commit func() error
  46. }
  47. func New(client client.Client, scheme *runtime.Scheme, namespace string,
  48. resource v1beta1.GeneratorStateManagingResource) *Manager {
  49. return &Manager{
  50. scheme: scheme,
  51. client: client,
  52. namespace: namespace,
  53. resource: resource,
  54. }
  55. }
  56. // Rollback will rollback the enqueued operations.
  57. func (m *Manager) Rollback() error {
  58. var errs []error
  59. for _, item := range m.internalState {
  60. if err := item.Rollback(); err != nil {
  61. errs = append(errs, err)
  62. }
  63. }
  64. return errors.Join(errs...)
  65. }
  66. // Commit will apply the enqueued changes to the state of the generators.
  67. func (m *Manager) Commit() error {
  68. var errs []error
  69. for _, item := range m.internalState {
  70. if err := item.Commit(); err != nil {
  71. errs = append(errs, err)
  72. }
  73. }
  74. return errors.Join(errs...)
  75. }
  76. // EnqueueFlagLatestStateForGC will flag the latest state for garbage collection after Commit.
  77. // It will be cleaned up later by the garbage collector.
  78. func (m *Manager) EnqueueFlagLatestStateForGC(stateKey string) {
  79. m.internalState = append(m.internalState, QueueItem{
  80. Commit: func() error {
  81. genState := m.resource.GetGeneratorState()
  82. if genState.Latest == nil {
  83. return nil
  84. }
  85. latest, ok := genState.Latest[stateKey]
  86. if !ok {
  87. return nil
  88. }
  89. gen, err := m.getGenerator(latest.Resource.Raw)
  90. if err != nil {
  91. return err
  92. }
  93. return m.moveStateToGC(latest.Resource, stateKey, gen, latest.State)
  94. },
  95. })
  96. }
  97. func (m *Manager) getGenerator(resource []byte) (v1alpha1.Generator, error) {
  98. us := &unstructured.Unstructured{}
  99. if err := us.UnmarshalJSON(resource); err != nil {
  100. return nil, fmt.Errorf("unable to unmarshal resource: %w", err)
  101. }
  102. ref := v1beta1.GeneratorRef{
  103. APIVersion: us.GetAPIVersion(),
  104. Kind: us.GetKind(),
  105. Name: us.GetName(),
  106. }
  107. gen, _, err := resolvers.GeneratorRef(context.TODO(), m.client, m.scheme, m.namespace, &ref)
  108. return gen, err
  109. }
  110. // EnqueueMoveStateToGC will move the generator state to GC if Commit() is called.
  111. func (m *Manager) EnqueueMoveStateToGC(resource *apiextensions.JSON, stateKey string, gen v1alpha1.Generator, state v1alpha1.GeneratorProviderState) {
  112. m.internalState = append(m.internalState, QueueItem{
  113. Commit: func() error {
  114. return m.moveStateToGC(resource, stateKey, gen, state)
  115. },
  116. })
  117. }
  118. // moveStateToGC moves the generator state to GC and enqueues it for cleanup.
  119. func (m *Manager) moveStateToGC(resource *apiextensions.JSON, stateKey string, gen v1alpha1.Generator, state v1alpha1.GeneratorProviderState) error {
  120. genState := m.resource.GetGeneratorState()
  121. entry := gc.Entry{
  122. Resource: resource,
  123. Impl: gen,
  124. State: state,
  125. }
  126. if err := gc.Enqueue(entry); err != nil {
  127. return fmt.Errorf("unable to enqueue generator state for GC: %w", err)
  128. }
  129. if genState.GC == nil {
  130. genState.GC = make(map[string]*v1beta1.GeneratorGCState)
  131. }
  132. genState.GC[gcGeneratorStateKey(entry, stateKey)] = &v1beta1.GeneratorGCState{
  133. Resource: resource,
  134. State: state,
  135. FlaggedForGCTime: metav1.Now(),
  136. }
  137. return nil
  138. }
  139. func gcGeneratorStateKey(entry gc.Entry, key string) string {
  140. return fmt.Sprintf("[%s]-%s", key, entry.Key())
  141. }
  142. // EnqueueSetLatest sets the latest state for the given key.
  143. // It will commit the state on success or move the state to GC on failure.
  144. func (m *Manager) EnqueueSetLatest(ctx context.Context, kubeClient client.Client, stateKey, namespace string, resource *apiextensions.JSON, gen v1alpha1.Generator, state v1alpha1.GeneratorProviderState) {
  145. m.internalState = append(m.internalState, QueueItem{
  146. // Store state at .Latest[<key>] on success
  147. // or attempt to immediately delete the state on failure
  148. Commit: func() error {
  149. genState := m.resource.GetGeneratorState()
  150. if genState.Latest == nil {
  151. genState.Latest = make(map[string]*v1beta1.GeneratorResourceState)
  152. }
  153. genState.Latest[stateKey] = &v1beta1.GeneratorResourceState{
  154. Resource: resource,
  155. State: state,
  156. }
  157. return nil
  158. },
  159. // Rollback by cleaning up the state.
  160. // In case of failure, move the state to GC so it will be cleaned up later.
  161. Rollback: func() error {
  162. err := gen.Cleanup(ctx, resource, state, kubeClient, namespace)
  163. if err == nil {
  164. return nil
  165. }
  166. return m.moveStateToGC(resource, fmt.Sprintf("rollback-%s", uuid.New().String()), gen, state)
  167. },
  168. })
  169. }
  170. // GetLatest returns the latest state for the given key.
  171. func (m *Manager) GetLatest(key string) *apiextensions.JSON {
  172. state := m.resource.GetGeneratorState()
  173. if state.Latest == nil {
  174. return nil
  175. }
  176. latest := state.Latest[key]
  177. if latest == nil {
  178. return nil
  179. }
  180. return latest.State
  181. }
  182. // CleanupImmediate will cleanup the generator state immediately.
  183. // This is useful when we want to cleanup the state immediately after deletion of the resource.
  184. func (m *Manager) CleanupImmediate(ctx context.Context, resource v1beta1.GeneratorStateManagingResource, kubeClient client.Client, namespace string) error {
  185. var errs []error
  186. generatorState := resource.GetGeneratorState()
  187. for _, gcState := range generatorState.GC {
  188. gen, err := m.getGenerator(gcState.Resource.Raw)
  189. if err != nil {
  190. errs = append(errs, fmt.Errorf("unable to get generator: %w", err))
  191. continue
  192. }
  193. err = gen.Cleanup(ctx, gcState.Resource, gcState.State, kubeClient, namespace)
  194. if err != nil {
  195. errs = append(errs, fmt.Errorf("failed to cleanup generator state: %w", err))
  196. }
  197. }
  198. return errors.Join(errs...)
  199. }
  200. // GarbageCollect will cleanup the generator state that is flagged for GC.
  201. // It updates the generator state with the new GC state.
  202. // If an error occurs during cleanup of a generator state,
  203. // it will be aggregated and returned at the end but the cleanup will continue for the remaining generator states.
  204. func (m *Manager) GarbageCollect(ctx context.Context, kubeClient client.Client, namespace string) error {
  205. var errs []error
  206. generatorState := m.resource.GetGeneratorState()
  207. newGCState := make(map[string]*v1beta1.GeneratorGCState)
  208. for idx, gcState := range generatorState.GC {
  209. gen, err := m.getGenerator(gcState.Resource.Raw)
  210. if err != nil {
  211. errs = append(errs, fmt.Errorf("unable to get generator: %w", err))
  212. continue
  213. }
  214. deleted, err := gc.Cleanup(ctx, gcState.FlaggedForGCTime.Time, gc.Entry{
  215. Resource: gcState.Resource,
  216. Impl: gen,
  217. State: gcState.State,
  218. }, kubeClient, namespace)
  219. if err != nil {
  220. errs = append(errs, fmt.Errorf("failed to cleanup generator state: %w", err))
  221. continue
  222. }
  223. if !deleted {
  224. newGCState[idx] = gcState
  225. }
  226. }
  227. generatorState.GC = newGCState
  228. m.resource.SetGeneratorState(*generatorState)
  229. return errors.Join(errs...)
  230. }