Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement tortoisectl stop command #400

Merged
merged 3 commits into from
May 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading