diff --git a/cmd/kill.go b/cmd/kill.go new file mode 100644 index 0000000..1c24ab3 --- /dev/null +++ b/cmd/kill.go @@ -0,0 +1,51 @@ +package cmd + +import ( + "errors" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +// killCmd represents the kill command +var killCmd = &cobra.Command{ + Use: "kill", + Short: "kill a pod", + Long: `This command find the corresponding pod and kill it`, + Args: cobra.MinimumNArgs(1), + Run: func(cmd *cobra.Command, args []string) { + errs := runKill(namespace, args) + if len(errs) > 0 { + for _, err := range errs { + logrus.Error(err.Error()) + } + logrus.Fatal("some pod could not be killed") + } + }, +} + +func NewKillCommand() *cobra.Command { + addCommonNamespaceCommandFlags(killCmd) + + return killCmd +} + +func runKill(namespace string, deployments []string) []error { + if namespace == "" { + return []error{errors.New("you must specified a namespace using the --namespace flag")} + } + + api := newAPI(newFileClient(dir), newKubernetesClient()) + + errs := api.Kill(namespace, deployments) + if len(errs) > 0 { + return errs + } + + logrus.WithFields(logrus.Fields{ + "namespace": namespace, + "deployments": deployments, + }).Info("pod are being killed") + + return nil +} diff --git a/cmd/root.go b/cmd/root.go index d11a1a5..75ccd61 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -65,6 +65,7 @@ func NewBlackbeardCommand() *cobra.Command { rootCmd.AddCommand(NewGetCommand()) rootCmd.AddCommand(NewResetCommand()) rootCmd.AddCommand(NewVersionCommand()) + rootCmd.AddCommand(NewKillCommand()) rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.blackbeard.yaml)") rootCmd.PersistentFlags().StringVar(&dir, "dir", "", "Use the specified dir as root path to execute commands. Default is the current dir.") diff --git a/pkg/api/api.go b/pkg/api/api.go index d1e3faa..fb1deca 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -1,12 +1,10 @@ package api import ( - "strings" "time" "github.com/Meetic/blackbeard/pkg/playbook" "github.com/Meetic/blackbeard/pkg/resource" - "github.com/Meetic/blackbeard/pkg/version" "github.com/sirupsen/logrus" ) @@ -19,6 +17,7 @@ type Api interface { Pods() resource.PodService Create(namespace string) (playbook.Inventory, error) Delete(namespace string, wait bool) error + Kill(namespace string, deployments []string) []error ListExposedServices(namespace string) ([]resource.Service, error) ListNamespaces() ([]Namespace, error) Reset(namespace string, configPath string) error @@ -211,23 +210,3 @@ func (api *api) WatchDelete() { } } } - -type Version struct { - Blackbeard string `json:"blackbeard"` - Kubernetes string `json:"kubernetes"` - Kubectl string `json:"kubectl"` -} - -func (api *api) GetVersion() (*Version, error) { - v, err := api.cluster.GetVersion() - - if err != nil { - return nil, err - } - - return &Version{ - Blackbeard: version.GetVersion(), - Kubernetes: strings.Join([]string{v.ClientVersion.Major, v.ClientVersion.Minor}, "."), - Kubectl: strings.Join([]string{v.ServerVersion.Major, v.ServerVersion.Minor}, "."), - }, nil -} diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index 8cd5fcc..bdccd89 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -20,7 +20,7 @@ var ( mock.NewConfigRepository(), mock.NewPlaybookRepository(), mock.NewNamespaceRepository(kube, false), - kubernetes.NewPodRepository(kube), + mock.NewPodRepository(kube), kubernetes.NewServiceRepository(kube, "kube.test"), kubernetes.NewClusterRepository(), ) @@ -42,7 +42,7 @@ func (clusterRepositoryMock) GetVersion() (*resource.Version, error) { } func TestGetVersion(t *testing.T) { - blackbeard = api.NewApi( + b := api.NewApi( mock.NewInventoryRepository(), mock.NewConfigRepository(), mock.NewPlaybookRepository(), @@ -52,7 +52,7 @@ func TestGetVersion(t *testing.T) { &clusterRepositoryMock{}, ) - version, err := blackbeard.GetVersion() + version, err := b.GetVersion() assert.Nil(t, err) assert.Equal(t, version, &api.Version{Blackbeard: "dev", Kubernetes: "1.2", Kubectl: "0.9"}) diff --git a/pkg/api/deployment.go b/pkg/api/deployment.go new file mode 100644 index 0000000..0ec733f --- /dev/null +++ b/pkg/api/deployment.go @@ -0,0 +1,25 @@ +package api + +func (api *api) Kill(namespace string, deployments []string) []error { + + var errors []error + + for _, d := range deployments { + pod, err := api.Pods().Find(namespace, d) + if err != nil { + errors = append(errors, err) + continue + } + + if err := api.Pods().Delete(namespace, pod); err != nil { + errors = append(errors, err) + continue + } + } + + if len(errors) > 0 { + return errors + } + + return nil +} diff --git a/pkg/api/deployment_test.go b/pkg/api/deployment_test.go new file mode 100644 index 0000000..dca16e7 --- /dev/null +++ b/pkg/api/deployment_test.go @@ -0,0 +1,17 @@ +package api_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKill(t *testing.T) { + errs := blackbeard.Kill("foo", []string{"test"}) + assert.Empty(t, errs) +} + +func TestKillKO(t *testing.T) { + errs := blackbeard.Kill("foo", []string{"bar"}) + assert.NotEmpty(t, errs) +} diff --git a/pkg/api/version.go b/pkg/api/version.go new file mode 100644 index 0000000..9210f7d --- /dev/null +++ b/pkg/api/version.go @@ -0,0 +1,27 @@ +package api + +import ( + "strings" + + "github.com/Meetic/blackbeard/pkg/version" +) + +type Version struct { + Blackbeard string `json:"blackbeard"` + Kubernetes string `json:"kubernetes"` + Kubectl string `json:"kubectl"` +} + +func (api *api) GetVersion() (*Version, error) { + v, err := api.cluster.GetVersion() + + if err != nil { + return nil, err + } + + return &Version{ + Blackbeard: version.GetVersion(), + Kubernetes: strings.Join([]string{v.ClientVersion.Major, v.ClientVersion.Minor}, "."), + Kubectl: strings.Join([]string{v.ServerVersion.Major, v.ServerVersion.Minor}, "."), + }, nil +} diff --git a/pkg/http/resource_handler.go b/pkg/http/resource_handler.go index ee65d61..17b5ef3 100644 --- a/pkg/http/resource_handler.go +++ b/pkg/http/resource_handler.go @@ -1,6 +1,7 @@ package http import ( + "fmt" "net/http" "github.com/gin-gonic/gin" @@ -80,3 +81,25 @@ func (h *Handler) GetStatuses(c *gin.Context) { c.JSON(http.StatusOK, statuses) } + +type killQuery []string + +// Kill handle the kill deployments +func (h *Handler) Kill(c *gin.Context) { + + var kQuery killQuery + + if err := c.BindJSON(&kQuery); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + // Kill the corresponding pods + errs := h.api.Kill(c.Params.ByName("namespace"), kQuery) + + if len(errs) > 0 { + c.JSON(http.StatusBadRequest, gin.H{"errors": fmt.Sprintf("%v", errs)}) + return + } + c.Status(http.StatusOK) +} diff --git a/pkg/http/server.go b/pkg/http/server.go index eb7ab89..852d327 100644 --- a/pkg/http/server.go +++ b/pkg/http/server.go @@ -56,6 +56,7 @@ func NewHandler(api api.Api, websocket WsHandler, configPath string, corsEnable h.engine.GET("/defaults", h.GetDefaults) h.engine.PUT("/inventories/:namespace", h.Update) h.engine.DELETE("/inventories/:namespace", h.Delete) + h.engine.DELETE("/inventories/:namespace/deployments", h.Kill) h.engine.GET("/ws", func(c *gin.Context) { websocket.Handle(c.Writer, c.Request) }) diff --git a/pkg/kubernetes/namespace.go b/pkg/kubernetes/namespace.go index 3287ec7..52c5533 100644 --- a/pkg/kubernetes/namespace.go +++ b/pkg/kubernetes/namespace.go @@ -147,16 +147,16 @@ func execute(c string, t time.Duration) error { if t > 0 { timer = time.NewTimer(t) var err error - go func(timer *time.Timer, cmd *exec.Cmd) { + go func(timer *time.Timer, cmd *exec.Cmd, err *error) { for range timer.C { e := cmd.Process.Kill() if e != nil { - err = errors.New("the command has timeout but the process could not be killed") + *err = errors.New("the command has timeout but the process could not be killed") } else { - err = errors.New("the command timed out") + *err = errors.New("the command timed out") } } - }(timer, cmd) + }(timer, cmd, &err) } err = cmd.Wait() diff --git a/pkg/kubernetes/pod.go b/pkg/kubernetes/pod.go index cd7c3e5..a5e8890 100644 --- a/pkg/kubernetes/pod.go +++ b/pkg/kubernetes/pod.go @@ -39,3 +39,7 @@ func (pr *podRepository) List(n string) (resource.Pods, error) { return pods, nil } + +func (pr *podRepository) Delete(n string, pod resource.Pod) error { + return pr.kubernetes.CoreV1().Pods(n).Delete(pod.Name, &metav1.DeleteOptions{}) +} diff --git a/pkg/mock/pod.go b/pkg/mock/pod.go index 2ea6a73..6a32472 100644 --- a/pkg/mock/pod.go +++ b/pkg/mock/pod.go @@ -1,8 +1,10 @@ package mock import ( + "errors" + "github.com/Meetic/blackbeard/pkg/resource" - "k8s.io/api/core/v1" + v1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) @@ -21,7 +23,6 @@ func NewPodRepository(kubernetes kubernetes.Interface) resource.PodRepository { // GetPods of all the pods in a given namespace. // This method returns a Pods slice containing the pod name and the pod status (pod status phase). func (rs *podRepository) List(n string) (resource.Pods, error) { - if n == "testko" { pods := resource.Pods{ @@ -44,3 +45,10 @@ func (rs *podRepository) List(n string) (resource.Pods, error) { return pods, nil } + +func (rs *podRepository) Delete(n string, p resource.Pod) error { + if p.Name == "err" { + return errors.New("an error occurred in pod deletion") + } + return nil +} diff --git a/pkg/resource/pod.go b/pkg/resource/pod.go index b9a884e..7edfdec 100644 --- a/pkg/resource/pod.go +++ b/pkg/resource/pod.go @@ -1,7 +1,10 @@ package resource import ( - "k8s.io/api/core/v1" + "fmt" + "strings" + + v1 "k8s.io/api/core/v1" ) // Pods represent a list of pods. @@ -23,12 +26,15 @@ type podService struct { // PodRepository represents the way Pods are managed type PodRepository interface { - List(string) (Pods, error) + List(namespace string) (Pods, error) + Delete(namespace string, pod Pod) error //Get(string, string) (Pod, error) } type PodService interface { - List(string) (Pods, error) + List(namespace string) (Pods, error) + Find(namespace, deployment string) (Pod, error) + Delete(namespace string, pod Pod) error } //NewPodService returns a new PodService @@ -42,3 +48,23 @@ func NewPodService(pods PodRepository) PodService { func (ps *podService) List(namespace string) (Pods, error) { return ps.pods.List(namespace) } + +func (ps *podService) Find(namespace, deployment string) (Pod, error) { + podList, err := ps.pods.List(namespace) + + if err != nil { + return Pod{}, err + } + + for _, pod := range podList { + if strings.Contains(pod.Name, deployment) { + return pod, nil + } + } + + return Pod{}, fmt.Errorf("no pod have been found for deployment name : %s", deployment) +} + +func (ps *podService) Delete(namespace string, pod Pod) error { + return ps.pods.Delete(namespace, pod) +} diff --git a/pkg/resource/pod_test.go b/pkg/resource/pod_test.go new file mode 100644 index 0000000..5c17cb3 --- /dev/null +++ b/pkg/resource/pod_test.go @@ -0,0 +1,32 @@ +package resource_test + +import ( + "testing" + + "github.com/Meetic/blackbeard/pkg/mock" + "github.com/Meetic/blackbeard/pkg/resource" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" +) + +var ( + pods = resource.NewPodService(mock.NewPodRepository(kube)) +) + +func TestFindOk(t *testing.T) { + p, err := pods.Find("foo", "te") + + assert.Contains(t, p.Name, "te") + assert.Nil(t, err) +} + +func TestFindNOk(t *testing.T) { + _, err := pods.Find("foo", "bar") + + assert.NotNil(t, err) +} + +func TestDeleteNOk(t *testing.T) { + err := pods.Delete("foo", resource.Pod{"err", v1.PodRunning}) + assert.NotNil(t, err) +} diff --git a/pkg/websocket/handler.go b/pkg/websocket/handler.go index 69f24c3..4366b78 100644 --- a/pkg/websocket/handler.go +++ b/pkg/websocket/handler.go @@ -59,7 +59,7 @@ func NewHandler(api api.Api) *handler { func (h *handler) Handle(w http.ResponseWriter, r *http.Request) { conn, err := h.upgrader.Upgrade(w, r, nil) if err != nil { - logrus.Warnf("Failed to set websocket upgrade: ", err) + logrus.Warnf("Failed to set websocket upgrade: %s", err) return }