diff --git a/actions/actions.go b/actions/actions.go index ea374309..2ccf0ad1 100644 --- a/actions/actions.go +++ b/actions/actions.go @@ -57,6 +57,7 @@ func NewService( reflect.TypeOf(&castai.ActionChartUninstall{}): newChartUninstallHandler(log, helmClient), reflect.TypeOf(&castai.ActionDisconnectCluster{}): newDisconnectClusterHandler(log, clientset), reflect.TypeOf(&castai.ActionSendAKSInitData{}): newSendAKSInitDataHandler(log, castaiClient), + reflect.TypeOf(&castai.ActionCheckNodeDeleted{}): newCheckNodeDeletedHandler(log, clientset), }, } } diff --git a/actions/check_node_deleted.go b/actions/check_node_deleted.go new file mode 100644 index 00000000..caffad0e --- /dev/null +++ b/actions/check_node_deleted.go @@ -0,0 +1,60 @@ +package actions + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/sirupsen/logrus" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + + "github.com/castai/cluster-controller/castai" +) + +type checkNodeDeletedConfig struct { + retries uint64 + retryWait time.Duration +} + +func newCheckNodeDeletedHandler(log logrus.FieldLogger, clientset kubernetes.Interface) ActionHandler { + return &checkNodeDeletedHandler{ + log: log, + clientset: clientset, + cfg: checkNodeDeletedConfig{ + retries: 5, + retryWait: 1 * time.Second, + }, + } +} + +type checkNodeDeletedHandler struct { + log logrus.FieldLogger + clientset kubernetes.Interface + cfg checkNodeDeletedConfig +} + +func (h *checkNodeDeletedHandler) Handle(ctx context.Context, data interface{}) error { + req, ok := data.(*castai.ActionCheckNodeDeleted) + if !ok { + return fmt.Errorf("unexpected type %T for check node deleted handler", data) + } + + log := h.log.WithField("node_name", req.NodeName) + log.Info("checking if node is deleted") + + b := backoff.WithContext(backoff.WithMaxRetries(backoff.NewConstantBackOff(h.cfg.retryWait), h.cfg.retries), ctx) + return backoff.Retry(func() error { + n, err := h.clientset.CoreV1().Nodes().Get(ctx, req.NodeName, metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + return nil + } + if n != nil { + return backoff.Permanent(errors.New("node is not deleted")) + } + return err + }, b) +} diff --git a/actions/check_node_handler_test.go b/actions/check_node_handler_test.go new file mode 100644 index 00000000..9bfdc3ce --- /dev/null +++ b/actions/check_node_handler_test.go @@ -0,0 +1,61 @@ +package actions + +import ( + "context" + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" + + "github.com/castai/cluster-controller/castai" +) + +func TestCheckNodeDeletedHandler(t *testing.T) { + r := require.New(t) + + log := logrus.New() + log.SetLevel(logrus.DebugLevel) + + t.Run("return error when node is not deleted", func(t *testing.T) { + nodeName := "node1" + node := &v1.Node{ + ObjectMeta: metav1.ObjectMeta{ + Name: nodeName, + }, + } + clientset := fake.NewSimpleClientset(node) + + h := checkNodeDeletedHandler{ + log: log, + clientset: clientset, + cfg: checkNodeDeletedConfig{}, + } + + req := &castai.ActionCheckNodeDeleted{ + NodeName: "node1", + } + + err := h.Handle(context.Background(), req) + r.EqualError(err, "node is not deleted") + }) + + t.Run("handle check successfully when node is not found", func(t *testing.T) { + clientset := fake.NewSimpleClientset() + + h := checkNodeDeletedHandler{ + log: log, + clientset: clientset, + cfg: checkNodeDeletedConfig{}, + } + + req := &castai.ActionCheckNodeDeleted{ + NodeName: "node1", + } + + err := h.Handle(context.Background(), req) + r.NoError(err) + }) +} diff --git a/castai/types.go b/castai/types.go index 6a4f720f..210b3ed5 100644 --- a/castai/types.go +++ b/castai/types.go @@ -27,6 +27,7 @@ type ClusterAction struct { ActionChartUninstall *ActionChartUninstall `json:"actionChartUninstall,omitempty"` ActionDisconnectCluster *ActionDisconnectCluster `json:"actionDisconnectCluster,omitempty"` ActionSendAKSInitData *ActionSendAKSInitData `json:"actionSendAksInitData,omitempty"` + ActionCheckNodeDeleted *ActionCheckNodeDeleted `json:"actionCheckNodeDeleted,omitempty"` CreatedAt time.Time `json:"createdAt"` DoneAt *time.Time `json:"doneAt,omitempty"` Error *string `json:"error,omitempty"` @@ -60,6 +61,9 @@ func (c *ClusterAction) Data() interface{} { if c.ActionSendAKSInitData != nil { return c.ActionSendAKSInitData } + if c.ActionCheckNodeDeleted != nil { + return c.ActionCheckNodeDeleted + } return nil } @@ -112,6 +116,10 @@ type ActionDisconnectCluster struct { type ActionSendAKSInitData struct { } +type ActionCheckNodeDeleted struct { + NodeName string `json:"nodeName"` +} + type ActionChartUpsert struct { Namespace string `json:"namespace"` ReleaseName string `json:"releaseName"`