Skip to content

Commit

Permalink
Implement tortoisectl stop command (#400)
Browse files Browse the repository at this point in the history
* Implement tortoisectl stop command

* add documentation

* fix lint
  • Loading branch information
sanposhiho authored May 7, 2024
1 parent ab706a7 commit dfc57b0
Show file tree
Hide file tree
Showing 59 changed files with 5,232 additions and 36 deletions.
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ test-debug: envtest ginkgo
test-update: envtest ginkgo
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" UPDATE_TESTCASES=true $(GINKGO) -r --fail-fast

.PHONY: test-tortoisectl
test-tortoisectl: envtest
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" go test -timeout 30s -v -run Test_TortoiseCtlStop ./cmd/tortoisectl/test/...

GINKGO ?= $(LOCALBIN)/ginkgo
GINKGO_VERSION ?= v2.1.4

Expand All @@ -81,6 +85,7 @@ $(GINKGO): $(LOCALBIN)
.PHONY: build
build: generate fmt vet ## Build manager binary.
go build -o bin/manager main.go
go build -o bin/tortoisectl cmd/tortoisectl/main.go

.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
Expand Down
2 changes: 1 addition & 1 deletion api/core/v1/pod_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func (h *PodWebhook) Default(ctx context.Context, obj runtime.Object) error {
return nil
}

h.podService.ModifyPodResource(pod, tortoise)
h.podService.ModifyPodSpecResource(&pod.Spec, tortoise)
pod.Annotations[annotation.PodMutationAnnotation] = fmt.Sprintf("this pod is mutated by tortoise (%s)", tortoise.Name)

return nil
Expand Down
21 changes: 21 additions & 0 deletions cmd/tortoisectl/commands/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package commands

import (
"fmt"
"os"

"github.com/spf13/cobra"
)

var rootCmd = &cobra.Command{
Use: "tortoisectl [COMMANDS]",
Short: "tortoisectl is a CLI for managing Tortoise",
Long: `tortoisectl is a CLI for managing Tortoise.`,
}

func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
121 changes: 121 additions & 0 deletions cmd/tortoisectl/commands/stop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package commands

import (
"fmt"
"os"
"path/filepath"

"github.com/spf13/cobra"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/homedir"
"sigs.k8s.io/controller-runtime/pkg/client"

autoscalingv1beta3 "github.com/mercari/tortoise/api/v1beta3"
"github.com/mercari/tortoise/pkg/deployment"
"github.com/mercari/tortoise/pkg/pod"
"github.com/mercari/tortoise/pkg/stoper"
)

var stopCmd = &cobra.Command{
Use: "stop tortoise1 tortoise2...",
Short: "stop tortoise(s) safely",
Long: `
stop is the command to temporarily turn off tortoise(s) easily and safely.
It's intended to be used when your application is facing issues that might be caused by tortoise.
Specifically, it changes the tortoise updateMode to "Off" and restarts the deployment to bring the pods back to the original resource requests.
Also, with the --no-lowering-resources flag, it patches the deployment directly
so that changing tortoise to Off won't result in lowering the resource request(s), damaging the service.
e.g., if the Deployment declares 1 CPU request, and the current Pods' request is 2 CPU mutated by Tortoise,
it'd patch the deployment to 2 CPU request to prevent a possible negative impact on the service.
`,
RunE: func(cmd *cobra.Command, args []string) error {
// validation
if stopAll {
if len(args) != 0 {
return fmt.Errorf("tortoise name shouldn't be specified because of --all flag")
}
} else {
if stopNamespace == "" {
return fmt.Errorf("namespace must be specified")
}
if len(args) == 0 {
return fmt.Errorf("tortoise name must be specified")
}
}

config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
return fmt.Errorf("failed to build config: %v", err)
}

client, err := client.New(config, client.Options{
Scheme: scheme,
})
if err != nil {
return fmt.Errorf("failed to create client: %v", err)
}

recorder := record.NewBroadcaster().NewRecorder(scheme, corev1.EventSource{Component: "tortoisectl"})
deploymentService := deployment.New(client, "", "", recorder)
podService, err := pod.New(map[string]int64{}, "", nil, nil)
if err != nil {
return fmt.Errorf("failed to create pod service: %v", err)
}

stoperService := stoper.New(client, deploymentService, podService)

opts := []stoper.StoprOption{}
if noLoweringResources {
opts = append(opts, stoper.NoLoweringResource)
}

err = stoperService.Stop(cmd.Context(), args, stopNamespace, stopAll, os.Stdout, opts...)
if err != nil {
return fmt.Errorf("failed to stop tortoise(s): %v", err)
}

return nil
},
}

var (
// namespace to stop tortoise(s) in
stopNamespace string
// stop all tortoises in the specified namespace, or in all namespaces if no namespace is specified.
stopAll bool
// Stop tortoise without lowering resource requests.
// If this flag is specified and the current Deployment's resource request(s) is lower than the current Pods' request mutated by Tortoise,
// this CLI patches the deployment so that changing tortoise to Off won't result in lowering the resource request(s), damaging the service.
noLoweringResources bool

// Path to KUBECONFIG
kubeconfig string

scheme = runtime.NewScheme()
)

func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(autoscalingv1beta3.AddToScheme(scheme))

rootCmd.AddCommand(stopCmd)

if home := homedir.HomeDir(); home != "" {
stopCmd.Flags().StringVar(&kubeconfig, "kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
} else {
stopCmd.Flags().StringVar(&kubeconfig, "kubeconfig", "", "absolute path to the kubeconfig file")
}

stopCmd.Flags().StringVarP(&stopNamespace, "namespace", "n", "", "namespace to stop tortoise(s) in")
stopCmd.Flags().BoolVarP(&stopAll, "all", "A", false, "stop all tortoises in the specified namespace, or in all namespaces if no namespace is specified.")
stopCmd.Flags().BoolVar(&noLoweringResources, "no-lowering-resources", false, `Stop tortoise without lowering resource requests.
If this flag is specified and the current Deployment's resource request(s) is lower than the current Pods' request mutated by Tortoise,
this CLI patches the deployment so that changing tortoise to Off won't result in lowering the resource request(s), damaging the service.`)
}
7 changes: 7 additions & 0 deletions cmd/tortoisectl/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package main

import "github.com/mercari/tortoise/cmd/tortoisectl/commands"

func main() {
commands.Execute()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
metadata:
name: mercari-app-a
namespace: success-all-in-all-namespace
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: mercari
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
annotations:
kubectl.kubernetes.io/restartedAt: updated
creationTimestamp: null
labels:
app: mercari
spec:
containers:
- image: awesome-mercari-app-image
imagePullPolicy: Always
name: app
resources:
requests:
cpu: "10"
memory: 10Gi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
- image: awesome-istio-proxy-image
imagePullPolicy: Always
name: istio-proxy
resources:
requests:
cpu: "4"
memory: 4Gi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
status: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
metadata:
name: mercari-app
namespace: success-all-in-all-namespace-2
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: mercari
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
annotations:
kubectl.kubernetes.io/restartedAt: updated
creationTimestamp: null
labels:
app: mercari
spec:
containers:
- image: awesome-mercari-app-image
imagePullPolicy: Always
name: app
resources:
requests:
cpu: "10"
memory: 10Gi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
- image: awesome-istio-proxy-image
imagePullPolicy: Always
name: istio-proxy
resources:
requests:
cpu: "4"
memory: 4Gi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
status: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
metadata:
name: mercari-app-b
namespace: success-all-in-all-namespace
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: mercari
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
annotations:
kubectl.kubernetes.io/restartedAt: updated
creationTimestamp: null
labels:
app: mercari
spec:
containers:
- image: awesome-mercari-app-image
imagePullPolicy: Always
name: app
resources:
requests:
cpu: "10"
memory: 10Gi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
- image: awesome-istio-proxy-image
imagePullPolicy: Always
name: istio-proxy
resources:
requests:
cpu: "4"
memory: 4Gi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
status: {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
metadata:
name: mercari-app-c
namespace: success-all-in-all-namespace
spec:
progressDeadlineSeconds: 600
replicas: 1
revisionHistoryLimit: 10
selector:
matchLabels:
app: mercari
strategy:
rollingUpdate:
maxSurge: 25%
maxUnavailable: 25%
type: RollingUpdate
template:
metadata:
annotations:
kubectl.kubernetes.io/restartedAt: updated
creationTimestamp: null
labels:
app: mercari
spec:
containers:
- image: awesome-mercari-app-image
imagePullPolicy: Always
name: app
resources:
requests:
cpu: "10"
memory: 10Gi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
- image: awesome-istio-proxy-image
imagePullPolicy: Always
name: istio-proxy
resources:
requests:
cpu: "4"
memory: 4Gi
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
dnsPolicy: ClusterFirst
restartPolicy: Always
schedulerName: default-scheduler
securityContext: {}
terminationGracePeriodSeconds: 30
status: {}
Loading

0 comments on commit dfc57b0

Please sign in to comment.