Implement schedule stop for unix

pull/9503/head
Priya Wadhwa 2020-10-20 13:45:59 -07:00
parent a282354419
commit a54ea82e3b
10 changed files with 307 additions and 2 deletions

View File

@ -35,13 +35,15 @@ import (
"k8s.io/minikube/pkg/minikube/out"
"k8s.io/minikube/pkg/minikube/out/register"
"k8s.io/minikube/pkg/minikube/reason"
"k8s.io/minikube/pkg/minikube/schedule"
"k8s.io/minikube/pkg/minikube/style"
"k8s.io/minikube/pkg/util/retry"
)
var (
stopAll bool
keepActive bool
stopAll bool
keepActive bool
scheduledStop string
)
// stopCmd represents the stop command
@ -55,6 +57,7 @@ var stopCmd = &cobra.Command{
func init() {
stopCmd.Flags().BoolVar(&stopAll, "all", false, "Set flag to stop all profiles (clusters)")
stopCmd.Flags().BoolVar(&keepActive, "keep-context-active", false, "keep the kube-context active after cluster is stopped. Defaults to false.")
stopCmd.Flags().StringVar(&scheduledStop, "schedule", "", "Set flag to stop cluster after a set amount of time (e.g. --schedule=5m)")
if err := viper.GetViper().BindPFlags(stopCmd.Flags()); err != nil {
exit.Error(reason.InternalFlagsBind, "unable to bind flags", err)
@ -81,6 +84,18 @@ func runStop(cmd *cobra.Command, args []string) {
profilesToStop = append(profilesToStop, cname)
}
if scheduledStop != "" {
duration, err := time.ParseDuration(scheduledStop)
if err != nil {
exit.Message(reason.Usage, "provided value {{.schedule}} to --schedule is not a valid Golang time.Duration", out.V{"schedule": scheduledStop})
}
if err := schedule.Daemonize(profilesToStop, duration); err != nil {
exit.Message(reason.DaemonizeError, "unable to daemonize: {{.err}}", out.V{"err": err.Error()})
}
klog.Infof("sleeping %s before completing stop...", duration.String())
time.Sleep(duration)
}
stoppedNodes := 0
for _, profile := range profilesToStop {
stoppedNodes = stopProfile(profile)

2
go.mod
View File

@ -7,6 +7,7 @@ require (
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 // indirect
github.com/Parallels/docker-machine-parallels v1.3.0
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
github.com/VividCortex/godaemon v0.0.0-20200629145737-581b70a8a603
github.com/blang/semver v3.5.0+incompatible
github.com/c4milo/gotoolkit v0.0.0-20170318115440-bcc06269efa9 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible
@ -24,6 +25,7 @@ require (
github.com/evanphx/json-patch v4.5.0+incompatible // indirect
github.com/go-ole/go-ole v1.2.4 // indirect
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/google/go-cmp v0.4.1
github.com/google/go-containerregistry v0.0.0-20200601195303-96cf69f03a3c
github.com/google/go-github v17.0.0+incompatible

2
go.sum
View File

@ -117,6 +117,8 @@ github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUW
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM=
github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA=
github.com/VividCortex/godaemon v0.0.0-20200629145737-581b70a8a603 h1:ZqOqBuBJ9QfCo2ErNFCVh5UXWtwXu01xb/WX1ND0rPM=
github.com/VividCortex/godaemon v0.0.0-20200629145737-581b70a8a603/go.mod h1:Y8CJ3IwPIAkMhv/rRUWIlczaeqd9ty9yrl+nc2AbaL4=
github.com/afbjorklund/go-containerregistry v0.0.0-20200902152226-fbad78ec2813 h1:0tskN1ipU/BBrpoEIy0rdZS9jf5+wdP6IMRak8Iu/YE=
github.com/afbjorklund/go-containerregistry v0.0.0-20200902152226-fbad78ec2813/go.mod h1:npTSyywOeILcgWqd+rvtzGWflIPPcBQhYoOONaY4ltM=
github.com/afbjorklund/go-getter v1.4.1-0.20190910175809-eb9f6c26742c h1:18gEt7qzn7CW7qMkfPTFyyotlPbvPQo9o4IDV8jZqP4=

View File

@ -71,6 +71,7 @@ type ClusterConfig struct {
Addons map[string]bool
VerifyComponents map[string]bool // map of components to verify and wait for after start.
StartHostTimeout time.Duration
ScheduledStop *ScheduledStopConfig
ExposedPorts []string // Only used by the docker and podman driver
}
@ -137,3 +138,9 @@ type VersionedExtraOption struct {
// flag is applied to
GreaterThanOrEqual semver.Version
}
// ScheduledStopConfig contains information around scheduled stop
type ScheduledStopConfig struct {
InitiationTime int64
Duration time.Duration
}

View File

@ -19,6 +19,7 @@ package localpath
import (
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"strings"
@ -86,6 +87,11 @@ func ClientCert(name string) string {
return new
}
// PID returns the path to the pid file used by profile for scheduled stop
func PID(profile string) string {
return path.Join(Profile(profile), "pid")
}
// ClientKey returns client certificate path, used by kubeconfig
func ClientKey(name string) string {
new := filepath.Join(Profile(name), "client.key")

View File

@ -114,6 +114,7 @@ var (
InternalYamlMarshal = Kind{ID: "MK_YAML_MARSHAL", ExitCode: ExProgramError}
InternalCredsNotFound = Kind{ID: "MK_CREDENTIALS_NOT_FOUND", ExitCode: ExProgramNotFound, Style: style.Shrug}
InternalSemverParse = Kind{ID: "MK_SEMVER_PARSE", ExitCode: ExProgramError}
DaemonizeError = Kind{ID: "MK_DAEMONIZE", ExitCode: ExProgramError}
RsrcInsufficientCores = Kind{ID: "RSRC_INSUFFICIENT_CORES", ExitCode: ExInsufficientCores, Style: style.UnmetRequirement}
RsrcInsufficientDarwinDockerCores = Kind{

View File

@ -0,0 +1,79 @@
// +build !windows
/*
Copyright 2020 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package schedule
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"time"
"github.com/VividCortex/godaemon"
"github.com/golang/glog"
"github.com/pkg/errors"
"k8s.io/minikube/pkg/minikube/localpath"
)
func killExistingScheduledStops(profiles []string) error {
for _, profile := range profiles {
file := localpath.PID(profile)
f, err := ioutil.ReadFile(file)
if os.IsNotExist(err) {
return nil
}
defer os.Remove(file)
if err != nil {
return errors.Wrapf(err, "reading %s", file)
}
pid, err := strconv.Atoi(string(f))
if err != nil {
return errors.Wrapf(err, "converting %v to int", string(f))
}
p, err := os.FindProcess(pid)
if err != nil {
return errors.Wrap(err, "finding process")
}
glog.Infof("killing process %v as it is an old scheduled stop", pid)
if err := p.Kill(); err != nil {
return errors.Wrapf(err, "killing %v", pid)
}
}
return nil
}
func daemonize(profiles []string, duration time.Duration) error {
_, _, err := godaemon.MakeDaemon(&godaemon.DaemonAttr{})
if err != nil {
return err
}
// now that this process has daemonized, it has a new PID
pid := os.Getpid()
return savePIDs(pid, profiles)
}
func savePIDs(pid int, profiles []string) error {
for _, p := range profiles {
file := localpath.PID(p)
if err := ioutil.WriteFile(file, []byte(fmt.Sprintf("%v", pid)), 0644); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,32 @@
// +build windows
/*
Copyright 2020 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package schedule
import (
"fmt"
"time"
)
func killExistingScheduledStops(profiles []string) error {
return fmt.Errorf("not yet implemented for windows")
}
func daemonize(profiles []string, duration time.Duration) error {
return fmt.Errorf("not yet implemented for windows")
}

View File

@ -0,0 +1,47 @@
/*
Copyright 2020 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package schedule
import (
"log"
"time"
"github.com/pkg/errors"
"k8s.io/minikube/pkg/minikube/config"
"k8s.io/minikube/pkg/minikube/mustload"
)
// Daemonize daemonizes minikube so that scheduled stop happens as expected
func Daemonize(profiles []string, duration time.Duration) error {
// save current time and expected duration in config
scheduledStop := &config.ScheduledStopConfig{
InitiationTime: time.Now().Unix(),
Duration: duration,
}
if err := killExistingScheduledStops(profiles); err != nil {
log.Printf("error killing existing scheduled stops: %v", err)
}
for _, p := range profiles {
_, cc := mustload.Partial(p)
cc.ScheduledStop = scheduledStop
if err := config.SaveProfile(p, cc); err != nil {
return errors.Wrap(err, "saving profile")
}
}
return daemonize(profiles, duration)
}

View File

@ -0,0 +1,114 @@
// +build integration
/*
Copyright 2020 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package integration
import (
"context"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strconv"
"syscall"
"testing"
"time"
"github.com/docker/machine/libmachine/state"
"k8s.io/minikube/pkg/minikube/localpath"
"k8s.io/minikube/pkg/util/retry"
)
func TestScheduledStop(t *testing.T) {
profile := UniqueProfileName("scheduled-stop")
ctx, cancel := context.WithTimeout(context.Background(), Minutes(5))
defer CleanupWithLogs(t, profile, cancel)
startMinikube(ctx, t, profile)
// schedule a stop for 5 min from now and make sure PID is created
scheduledStopMinikube(ctx, t, profile, "5m")
pid := checkPID(t, profile)
if !processRunning(t, pid) {
t.Fatalf("process %v is not running", pid)
}
// redo scheduled stop to be 3 min
scheduledStopMinikube(ctx, t, profile, "10s")
if processRunning(t, pid) {
t.Fatalf("process %v running but should have been killed on reschedule of stop", pid)
}
checkPID(t, profile)
// wait allotted time to make sure minikube status is "Stopped"
time.Sleep(15 * time.Second)
checkStatus := func() error {
got := Status(ctx, t, Target(), profile, "Host", profile)
if got != state.Stopped.String() {
return fmt.Errorf("expected post-stop host status to be -%q- but got *%q*", state.Stopped, got)
}
return nil
}
if err := retry.Expo(checkStatus, 100*time.Microsecond, 5*time.Second); err != nil {
t.Fatalf("error %v", err)
}
}
func startMinikube(ctx context.Context, t *testing.T, profile string) {
args := append([]string{"start", "-p", profile}, StartArgs()...)
rr, err := Run(t, exec.CommandContext(ctx, Target(), args...))
if err != nil {
t.Fatalf("starting minikube: %v\n%s", err, rr.Output())
}
}
func scheduledStopMinikube(ctx context.Context, t *testing.T, profile string, stop string) {
args := []string{"stop", "-p", profile, "--schedule", stop}
rr, err := Run(t, exec.CommandContext(ctx, Target(), args...))
if err != nil {
t.Fatalf("starting minikube: %v\n%s", err, rr.Output())
}
}
func checkPID(t *testing.T, profile string) string {
file := localpath.PID(profile)
var contents []byte
getContents := func() error {
var err error
contents, err = ioutil.ReadFile(file)
return err
}
// first, make sure the PID file exists
if err := retry.Expo(getContents, 100*time.Microsecond, time.Minute*1); err != nil {
t.Fatalf("error reading %s: %v", file, err)
}
return string(contents)
}
func processRunning(t *testing.T, pid string) bool {
// make sure PID file contains a running process
p, err := strconv.Atoi(pid)
if err != nil {
return false
}
process, err := os.FindProcess(p)
if err != nil {
return false
}
err = process.Signal(syscall.Signal(0))
t.Log("signal error was: ", err)
return err == nil
}