-
Notifications
You must be signed in to change notification settings - Fork 160
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
examples: err = action.New(c). WithMatcher(matcher). WithAction(actionUpdate). Exec(ctx, d...) if err != nil { return err } return action.New(c). WithAction(action.Delete). ExecWithRetry(ctx, action.IfAnyLeft(action.DefaultMatcher), d...) Signed-off-by: Yauheni Kaliuta <[email protected]>
- Loading branch information
Showing
1 changed file
with
212 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
package action | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/hashicorp/go-multierror" | ||
|
||
"sigs.k8s.io/controller-runtime/pkg/client" | ||
|
||
"k8s.io/apimachinery/pkg/api/meta" | ||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" | ||
"k8s.io/apimachinery/pkg/runtime/schema" | ||
"k8s.io/apimachinery/pkg/util/wait" | ||
) | ||
|
||
type ResourceSpec struct { | ||
Gvk schema.GroupVersionKind | ||
Namespace string | ||
// path to the field, like "metadata", "name" | ||
Path []string | ||
// set of values for the field to match object, any one matches | ||
Values []string | ||
} | ||
|
||
type MatcherFunc func(r ResourceSpec, obj *unstructured.Unstructured) (bool, error) | ||
type ActionFunc func(ctx context.Context, c client.Client, r ResourceSpec, obj *unstructured.Unstructured) error | ||
type RetryCheckFunc func(ctx context.Context, c client.Client, resources ...ResourceSpec) (bool, error) | ||
|
||
type Action struct { | ||
client client.Client | ||
|
||
matcher MatcherFunc | ||
actions []ActionFunc | ||
} | ||
|
||
// shouldn't just return false on error? | ||
func DefaultMatcher(r ResourceSpec, obj *unstructured.Unstructured) (bool, error) { | ||
if len(r.Path) == 0 || len(r.Values) == 0 { | ||
return true, nil | ||
} | ||
|
||
v, ok, err := unstructured.NestedString(obj.Object, r.Path...) | ||
if err != nil { | ||
return false, fmt.Errorf("failed to get field %v for %s %s/%s: %w", r.Path, r.Gvk.Kind, r.Namespace, obj.GetName(), err) | ||
} | ||
|
||
if !ok { | ||
return false, fmt.Errorf("unexisting path to handle: %v", r.Path) | ||
} | ||
|
||
for _, toDelete := range r.Values { | ||
if v == toDelete { | ||
return true, nil | ||
} | ||
} | ||
|
||
return false, nil | ||
} | ||
|
||
func New(c client.Client) *Action { | ||
return &Action{ | ||
client: c, | ||
matcher: DefaultMatcher, | ||
} | ||
} | ||
|
||
func (o *Action) Or(m1, m2 MatcherFunc) MatcherFunc { | ||
return func(r ResourceSpec, obj *unstructured.Unstructured) (bool, error) { | ||
res, err := m1(r, obj) | ||
if err != nil { | ||
return false, err | ||
} | ||
if res { | ||
return true, err | ||
} | ||
return m2(r, obj) | ||
} | ||
} | ||
|
||
func (o *Action) And(m1, m2 MatcherFunc) MatcherFunc { | ||
return func(r ResourceSpec, obj *unstructured.Unstructured) (bool, error) { | ||
res, err := m1(r, obj) | ||
if err != nil { | ||
return false, err | ||
} | ||
if !res { | ||
return false, err | ||
} | ||
return m2(r, obj) | ||
} | ||
} | ||
|
||
func (o *Action) WithMatcher(m MatcherFunc) *Action { | ||
o.matcher = m | ||
return o | ||
} | ||
|
||
func (o *Action) WithAction(a ActionFunc) *Action { | ||
o.actions = append(o.actions, a) | ||
return o | ||
} | ||
|
||
func (o *Action) execOneResource(ctx context.Context, r ResourceSpec, objs []*unstructured.Unstructured) error { | ||
for _, item := range objs { | ||
for _, a := range o.actions { | ||
err := a(ctx, o.client, r, item) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func ListMatched(ctx context.Context, c client.Client, matcher MatcherFunc, resources ...ResourceSpec) (map[*ResourceSpec][]*unstructured.Unstructured, error) { | ||
ret := make(map[*ResourceSpec][]*unstructured.Unstructured) | ||
|
||
for _, r := range resources { | ||
r := r | ||
var items []*unstructured.Unstructured | ||
|
||
list := &unstructured.UnstructuredList{} | ||
list.SetGroupVersionKind(r.Gvk) | ||
|
||
err := c.List(ctx, list, client.InNamespace(r.Namespace)) | ||
if err != nil { | ||
if errors.Is(err, &meta.NoKindMatchError{}) { | ||
fmt.Printf("Could not list %v: CRD not found\n", r.Gvk) | ||
continue | ||
} else { | ||
return ret, fmt.Errorf("failed to list %s: %w", r.Gvk.Kind, err) | ||
} | ||
} | ||
|
||
for _, item := range list.Items { | ||
item := item | ||
|
||
matched, err := matcher(r, &item) | ||
if err != nil { | ||
return ret, err | ||
} | ||
|
||
if !matched { | ||
continue | ||
} | ||
|
||
items = append(items, &item) | ||
} | ||
|
||
if len(items) > 0 { | ||
ret[&r] = items | ||
} | ||
} | ||
|
||
return ret, nil | ||
} | ||
|
||
func (o *Action) Exec(ctx context.Context, resources ...ResourceSpec) error { | ||
var errors *multierror.Error | ||
|
||
matched, err := ListMatched(ctx, o.client, o.matcher, resources...) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
for r, objs := range matched { | ||
err := o.execOneResource(ctx, *r, objs) | ||
errors = multierror.Append(errors, err) | ||
} | ||
|
||
return errors.ErrorOrNil() | ||
} | ||
|
||
func (o *Action) ExecWithRetry(ctx context.Context, shouldRetry RetryCheckFunc, resources ...ResourceSpec) error { | ||
return wait.ExponentialBackoffWithContext(ctx, wait.Backoff{ | ||
// 5, 10, ,20, 40 then timeout | ||
Duration: 5 * time.Second, | ||
Factor: 2.0, | ||
Jitter: 0.1, | ||
Steps: 4, | ||
Cap: 1 * time.Minute, | ||
}, func(ctx context.Context) (bool, error) { | ||
err := o.Exec(ctx, resources...) | ||
if err != nil { | ||
return false, err | ||
} | ||
return shouldRetry(ctx, o.client, resources...) | ||
}) | ||
} | ||
|
||
func (o *Action) DryRun(_ context.Context, _ ...ResourceSpec) error { | ||
return nil | ||
} | ||
|
||
func Delete(ctx context.Context, c client.Client, r ResourceSpec, obj *unstructured.Unstructured) error { | ||
return c.Delete(ctx, obj) | ||
} | ||
|
||
func IfAnyLeft(matcher MatcherFunc) RetryCheckFunc { | ||
return func(ctx context.Context, c client.Client, resources ...ResourceSpec) (bool, error) { | ||
matched, err := ListMatched(ctx, c, matcher, resources...) | ||
if err != nil { | ||
return false, nil | ||
} | ||
|
||
return len(matched) == 0, nil | ||
} | ||
} |