package actions import ( "context" "errors" "fmt" "slices" "time" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "github.com/sophotechlabs/spinoza/internal/api" ) const ( fieldManager = "spinoza" appsGroup = "apps" batchGroup = "batch" specField = "spec" ) type Action string const ( Scale Action = "scale" Restart Action = "restart" Cordon Action = "cordon" Uncordon Action = "uncordon" Drain Action = "drain" Suspend Action = "suspend" Resume Action = "resume" Trigger Action = "trigger" Undo Action = "undo" ) var ErrUnsupported = errors.New("action is not supported for this resource") type groupResource struct { group string resource string } var scalable = map[groupResource]bool{ {group: appsGroup, resource: "deployments"}: true, {group: appsGroup, resource: "statefulsets"}: true, {group: appsGroup, resource: "replicasets"}: true, {group: "", resource: "replicationcontrollers"}: true, } var restartable = map[groupResource]bool{ {group: appsGroup, resource: "deployments"}: true, {group: appsGroup, resource: "statefulsets"}: true, {group: appsGroup, resource: "daemonsets"}: true, } var revertible = map[groupResource]bool{ {group: appsGroup, resource: "deployments"}: true, {group: appsGroup, resource: "statefulsets"}: true, {group: appsGroup, resource: "daemonsets"}: true, } var cronJobs = groupResource{group: batchGroup, resource: "cronjobs"} type Request struct { Ref api.ObjectRef Action Action Replicas int64 Revision int64 Force bool DryRun bool } type Service struct { dyn dynamic.Interface cs kubernetes.Interface retryDelay time.Duration } const evictRetryDelay = 2 * time.Second func New(dyn dynamic.Interface, cs kubernetes.Interface) *Service { return newWithDelay(dyn, cs, evictRetryDelay) } func newWithDelay(dyn dynamic.Interface, cs kubernetes.Interface, retryDelay time.Duration) *Service { return &Service{dyn: dyn, cs: cs, retryDelay: retryDelay} } func Supported(ref api.ObjectRef, action Action) bool { key := groupResource{group: ref.Group, resource: ref.Resource} switch action { case Scale: return scalable[key] case Restart: return restartable[key] case Cordon, Uncordon, Drain: return key == groupResource{group: "", resource: "nodes"} case Suspend, Resume, Trigger: return key == cronJobs case Undo: return revertible[key] default: return false } } func Every() []Action { return []Action{Scale, Restart, Cordon, Uncordon, Drain, Suspend, Resume, Trigger, Undo} } func known(action Action) bool { return slices.Contains(Every(), action) } func (s *Service) Do(ctx context.Context, req Request, now time.Time) (api.ActionResult, error) { if req.Ref.Name == "" { return api.ActionResult{}, errors.New("name is required") } if !known(req.Action) { return api.ActionResult{}, fmt.Errorf("unknown action %q", req.Action) } if !Supported(req.Ref, req.Action) { return api.ActionResult{}, fmt.Errorf("%w: %s on %s", ErrUnsupported, req.Action, describe(req.Ref)) } switch req.Action { case Scale: return s.scale(ctx, req) case Restart: return s.restart(ctx, req, now) case Cordon: return s.setSchedulable(ctx, req.Ref, false) case Uncordon: return s.setSchedulable(ctx, req.Ref, true) case Drain: return s.drain(ctx, req) case Suspend: return s.setSuspended(ctx, req.Ref, true) case Resume: return s.setSuspended(ctx, req.Ref, false) case Trigger: return s.trigger(ctx, req.Ref) case Undo: return s.undo(ctx, req) default: return api.ActionResult{}, fmt.Errorf("unknown action %q", req.Action) } } func describe(ref api.ObjectRef) string { if ref.Group == "" { return ref.Resource } return ref.Group + "/" + ref.Resource }