Skip to content

Commit a13be58

Browse files
committed
feat: add DevSpace sleep mode handling
1 parent 820bc39 commit a13be58

10 files changed

Lines changed: 176 additions & 83 deletions

File tree

pkg/devspace/config/loader/imports.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,15 @@ package loader
33
import (
44
"context"
55
"fmt"
6-
"io/ioutil"
7-
"path/filepath"
8-
96
"github.com/loft-sh/devspace/pkg/devspace/config/loader/variable"
107
"github.com/loft-sh/devspace/pkg/devspace/config/versions"
118
"github.com/loft-sh/devspace/pkg/devspace/config/versions/util"
129
dependencyutil "github.com/loft-sh/devspace/pkg/devspace/dependency/util"
1310
"github.com/loft-sh/devspace/pkg/util/log"
1411
"github.com/loft-sh/devspace/pkg/util/yamlutil"
1512
"github.com/pkg/errors"
13+
"io/ioutil"
14+
"path/filepath"
1615
)
1716

1817
var ImportSections = []string{

pkg/devspace/config/loader/loader.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ package loader
33
import (
44
"context"
55
"fmt"
6+
"github.com/loft-sh/devspace/pkg/devspace/config/localcache"
7+
"github.com/loft-sh/devspace/pkg/devspace/config/remotecache"
68
"github.com/loft-sh/devspace/pkg/devspace/context/values"
9+
"github.com/loft-sh/devspace/pkg/devspace/kubectl"
10+
"github.com/loft-sh/devspace/pkg/util/command"
711
"github.com/loft-sh/devspace/pkg/util/encoding"
812
"github.com/loft-sh/devspace/pkg/util/yamlutil"
913
"io/ioutil"
@@ -13,11 +17,6 @@ import (
1317
"regexp"
1418
"strings"
1519

16-
"github.com/loft-sh/devspace/pkg/devspace/config/localcache"
17-
"github.com/loft-sh/devspace/pkg/devspace/config/remotecache"
18-
"github.com/loft-sh/devspace/pkg/devspace/kubectl"
19-
"github.com/loft-sh/devspace/pkg/util/command"
20-
2120
"github.com/loft-sh/devspace/pkg/util/constraint"
2221

2322
"github.com/loft-sh/devspace/pkg/devspace/plugin"

pkg/devspace/config/loader/parser.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func fillVariablesExcludeAndParse(ctx context.Context, resolver variable.Resolve
125125
}
126126

127127
func fillVariablesAndParse(ctx context.Context, resolver variable.Resolver, preparedConfig map[string]interface{}, log log.Logger, excludedPaths ...string) (*latest.Config, map[string]interface{}, error) {
128-
// fill in variables and expressions (leave out
128+
// fill in variables and expressions
129129
preparedConfigInterface, err := resolver.FillVariablesExclude(ctx, preparedConfig, excludedPaths)
130130
if err != nil {
131131
return nil, nil, err

pkg/devspace/devpod/devpod.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,9 @@ func (d *devPod) startWithRetry(ctx devspacecontext.Context, devPodConfig *lates
166166
return true, nil
167167
}, ctx.Context().Done())
168168
if err != nil {
169-
ctx.Log().Errorf("error restarting dev: %v", err)
169+
if err != wait.ErrWaitTimeout {
170+
ctx.Log().Errorf("error restarting dev: %v", err)
171+
}
170172
} else if shouldRestart {
171173
d.restart(ctx, devPodConfig, options)
172174
return

pkg/devspace/kubectl/client.go

Lines changed: 156 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,16 @@ import (
66
"github.com/loft-sh/devspace/pkg/devspace/config/localcache"
77
"github.com/loft-sh/devspace/pkg/devspace/kubectl/util"
88
"github.com/loft-sh/devspace/pkg/devspace/upgrade"
9-
"io"
10-
"net"
11-
"net/url"
12-
"os"
13-
"sort"
14-
9+
"github.com/loft-sh/devspace/pkg/util/idle"
1510
"github.com/loft-sh/devspace/pkg/util/kubeconfig"
1611
"github.com/loft-sh/devspace/pkg/util/log"
1712
"github.com/loft-sh/devspace/pkg/util/survey"
1813
"github.com/loft-sh/devspace/pkg/util/terminal"
14+
"io"
15+
"k8s.io/apimachinery/pkg/util/wait"
16+
"net/http"
17+
"os"
18+
"time"
1919

2020
"github.com/mgutz/ansi"
2121
"github.com/pkg/errors"
@@ -27,6 +27,10 @@ import (
2727
"k8s.io/client-go/tools/clientcmd"
2828
)
2929

30+
var (
31+
stopPingAfter = time.Second * 600
32+
)
33+
3034
// Client holds all kubernetes related functions
3135
type Client interface {
3236
// CurrentContext returns the current kube context name
@@ -108,6 +112,17 @@ func NewClientFromContext(context, namespace string, switchContext bool, kubeLoa
108112
return nil, err
109113
}
110114
restConfig.UserAgent = "DevSpace Version " + upgrade.GetVersion()
115+
restConfig.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
116+
return &devSpaceRoundTripper{
117+
roundTripper: rt,
118+
requestType: "Regular",
119+
callback: func(response *http.Response) {
120+
if response.Header.Get("X-DevSpace-Response-Type") == "Blocked" {
121+
log.GetInstance().Fatalf("Targeted Kubernetes environment has begun sleeping. Please restart DevSpace to wake up the environment")
122+
}
123+
},
124+
}
125+
}
111126

112127
kubeClient, err := kubernetes.NewForConfig(restConfig)
113128
if err != nil {
@@ -126,56 +141,6 @@ func NewClientFromContext(context, namespace string, switchContext bool, kubeLoa
126141
}, nil
127142
}
128143

129-
// NewClientBySelect creates a new kubernetes client by user select @Factory
130-
func NewClientBySelect(allowPrivate bool, switchContext bool, kubeLoader kubeconfig.Loader, log log.Logger) (Client, error) {
131-
kubeConfig, err := kubeLoader.LoadRawConfig()
132-
if err != nil {
133-
return nil, err
134-
}
135-
136-
// Get all kube contexts
137-
options := make([]string, 0, len(kubeConfig.Contexts))
138-
for context := range kubeConfig.Contexts {
139-
options = append(options, context)
140-
}
141-
if len(options) == 0 {
142-
return nil, errors.New("No kubectl context found. Make sure kubectl is installed and you have a working kubernetes context configured")
143-
}
144-
145-
sort.Strings(options)
146-
for {
147-
kubeContext, err := log.Question(&survey.QuestionOptions{
148-
Question: "Which kube context do you want to use",
149-
DefaultValue: kubeConfig.CurrentContext,
150-
Options: options,
151-
})
152-
if err != nil {
153-
return nil, err
154-
}
155-
156-
// Check if cluster is in private network
157-
if !allowPrivate {
158-
context := kubeConfig.Contexts[kubeContext]
159-
cluster := kubeConfig.Clusters[context.Cluster]
160-
161-
url, err := url.Parse(cluster.Server)
162-
if err != nil {
163-
return nil, errors.Wrap(err, "url parse")
164-
}
165-
166-
ip := net.ParseIP(url.Hostname())
167-
if ip != nil {
168-
if IsPrivateIP(ip) {
169-
log.Infof("Clusters with private ips (%s) cannot be used", url.Hostname())
170-
continue
171-
}
172-
}
173-
}
174-
175-
return NewClientFromContext(kubeContext, "", switchContext, kubeLoader)
176-
}
177-
}
178-
179144
// ClientConfig returns the underlying kube client config
180145
func (client *client) ClientConfig() clientcmd.ClientConfig {
181146
return client.clientConfig
@@ -312,7 +277,17 @@ func CheckKubeContext(client Client, localCache localcache.Cache, noWarning, aut
312277
log.Infof("Using namespace '%s'", ansi.Color(currentConfigContext.Namespace, "white+b"))
313278
log.Infof("Using kube context '%s'", ansi.Color(currentConfigContext.Context, "white+b"))
314279
if resetClient {
315-
return NewClientFromContext(currentConfigContext.Context, currentConfigContext.Namespace, true, client.KubeConfigLoader())
280+
var err error
281+
client, err = NewClientFromContext(currentConfigContext.Context, currentConfigContext.Namespace, true, client.KubeConfigLoader())
282+
if err != nil {
283+
return nil, err
284+
}
285+
}
286+
287+
// wake up and ping
288+
err := wakeUpAndPing(context.TODO(), client, log)
289+
if err != nil {
290+
return nil, errors.Wrap(err, "wakeup environment")
316291
}
317292

318293
return client, nil
@@ -337,3 +312,127 @@ func (client *client) RestConfig() *rest.Config {
337312
func (client *client) KubeConfigLoader() kubeconfig.Loader {
338313
return client.kubeLoader
339314
}
315+
316+
func wakeUpAndPing(ctx context.Context, client Client, log log.Logger) error {
317+
err := wakeUp(ctx, client, log)
318+
if err != nil {
319+
return err
320+
}
321+
322+
// create ping config
323+
pingConfig := rest.CopyConfig(client.RestConfig())
324+
pingConfig.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
325+
return &devSpaceRoundTripper{
326+
roundTripper: rt,
327+
requestType: "Ping",
328+
callback: func(response *http.Response) {
329+
if response.Header.Get("X-DevSpace-Response-Type") == "Blocked" {
330+
log.Fatalf("Targeted Kubernetes environment has begun sleeping. Please restart DevSpace to wake up the environment")
331+
}
332+
},
333+
}
334+
}
335+
336+
// create kube client
337+
kubeClient, err := kubernetes.NewForConfig(pingConfig)
338+
if err != nil {
339+
return err
340+
}
341+
342+
// start pinging
343+
go func() {
344+
getter, _ := idle.NewIdleGetter()
345+
wait.UntilWithContext(ctx, func(ctx context.Context) {
346+
if getter != nil {
347+
amountIdle, err := getter.Idle()
348+
if err == nil && amountIdle > stopPingAfter {
349+
return
350+
}
351+
}
352+
353+
_, err = kubeClient.CoreV1().Pods(client.Namespace()).List(ctx, metav1.ListOptions{LabelSelector: "devspace=ping"})
354+
if err != nil {
355+
log.Debugf("Error pinging Kubernetes environment: %v", err)
356+
}
357+
}, time.Minute*3)
358+
}()
359+
360+
return nil
361+
}
362+
363+
func wakeUp(ctx context.Context, client Client, log log.Logger) error {
364+
// check if environment is sleeping
365+
var isSleeping bool
366+
isSleepingConfig := rest.CopyConfig(client.RestConfig())
367+
isSleepingConfig.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
368+
return &devSpaceRoundTripper{
369+
roundTripper: rt,
370+
requestType: "Ping",
371+
callback: func(response *http.Response) {
372+
if response.Header.Get("X-DevSpace-Response-Type") == "Blocked" {
373+
isSleeping = true
374+
}
375+
},
376+
}
377+
}
378+
379+
// create kube client
380+
kubeClient, err := kubernetes.NewForConfig(isSleepingConfig)
381+
if err != nil {
382+
return err
383+
}
384+
385+
// wake up the environment
386+
_, err = kubeClient.CoreV1().Pods(client.Namespace()).List(ctx, metav1.ListOptions{LabelSelector: "devspace=wakeup"})
387+
if err != nil && !isSleeping {
388+
return fmt.Errorf("Please make sure you have an existing valid kube config. You might want to check one of the following things:\n\n* Make sure you can use 'kubectl get namespaces' locally\n* If you are using Loft, you might want to run 'devspace create space' or 'loft create space'")
389+
} else if !isSleeping {
390+
return nil
391+
}
392+
393+
// wake up the environment
394+
wakeUpConfig := rest.CopyConfig(client.RestConfig())
395+
wakeUpConfig.WrapTransport = func(rt http.RoundTripper) http.RoundTripper {
396+
return &devSpaceRoundTripper{
397+
roundTripper: rt,
398+
requestType: "WakeUp",
399+
callback: func(response *http.Response) {
400+
if response.Header.Get("X-DevSpace-Response-Type") == "WokenUp" {
401+
log.Infof("Successfully woken up Kubernetes environment")
402+
}
403+
},
404+
}
405+
}
406+
407+
// create kube client
408+
kubeClient, err = kubernetes.NewForConfig(wakeUpConfig)
409+
if err != nil {
410+
return err
411+
}
412+
413+
// print message if it takes too long
414+
log.Infof("DevSpace is waking up the Kubernetes environment, please wait a second...")
415+
416+
// wake up the environment
417+
_, err = kubeClient.CoreV1().Pods(client.Namespace()).List(ctx, metav1.ListOptions{LabelSelector: "devspace=wakeup"})
418+
if err != nil {
419+
return errors.Wrap(err, "error waking up the environment")
420+
}
421+
422+
return nil
423+
}
424+
425+
type devSpaceRoundTripper struct {
426+
roundTripper http.RoundTripper
427+
requestType string
428+
callback func(response *http.Response)
429+
}
430+
431+
func (d *devSpaceRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
432+
req.Header.Set("X-DevSpace-Request-Type", d.requestType)
433+
response, err := d.roundTripper.RoundTrip(req)
434+
if response != nil && d.callback != nil {
435+
d.callback(response)
436+
}
437+
return response, err
438+
}

pkg/devspace/kubectl/util/util.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
package util
22

33
import (
4-
"io/ioutil"
5-
"os"
6-
74
"github.com/loft-sh/devspace/pkg/util/kubeconfig"
85
"github.com/pkg/errors"
6+
"io/ioutil"
97
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
108
"k8s.io/client-go/rest"
119
"k8s.io/client-go/tools/clientcmd"
1210
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
11+
"os"
1312
)
1413

1514
const localContext = "incluster"

pkg/devspace/services/logs/logs.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ func StartLogs(
6565
lines = devContainer.Logs.LastLines
6666
}
6767

68+
ctx.Log().Infof("Streaming logs of pod:container %s:%s", ansi.Color(containerObj.Pod.Name, "white+b"), ansi.Color(containerObj.Container.Name, "white+b"))
6869
reader, err := ctx.KubeClient().Logs(ctx.Context(), containerObj.Pod.Namespace, containerObj.Pod.Name, containerObj.Container.Name, false, &lines, true)
6970
if err != nil {
7071
return err

pkg/util/downloader/commands/kubectl.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,13 @@ func (k *kubectlCommand) DownloadURL() string {
6363
}
6464

6565
func (k *kubectlCommand) IsValid(ctx context.Context, path string) (bool, error) {
66-
out, err := command.Output(ctx, "", expand.ListEnviron(os.Environ()...), path, "version", "--client")
66+
environ := []string{}
67+
environ = append(environ, os.Environ()...)
68+
// this is a hack because kubectl sometimes tries to reach the server anyways, so
69+
// we make sure this is not gonna actually contact the server
70+
environ = append(environ, "KUBECONFIG=does-not-exist.yaml")
71+
72+
out, err := command.Output(ctx, "", expand.ListEnviron(environ...), path, "version", "--client")
6773
if err != nil {
6874
return false, nil
6975
}

pkg/util/factory/factory.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ type Factory interface {
3131
// NewKubeDefaultClient creates a new kube client
3232
NewKubeDefaultClient() (kubectl.Client, error)
3333
NewKubeClientFromContext(context, namespace string) (kubectl.Client, error)
34-
NewKubeClientBySelect(allowPrivate bool, switchContext bool, log log.Logger) (kubectl.Client, error)
3534

3635
// NewHelmClient creates a new helm client
3736
NewHelmClient(log log.Logger) (types.Client, error)
@@ -140,12 +139,6 @@ func (f *DefaultFactoryImpl) NewKubeClientFromContext(context, namespace string)
140139
return client, nil
141140
}
142141

143-
// NewKubeClientBySelect implements interface
144-
func (f *DefaultFactoryImpl) NewKubeClientBySelect(allowPrivate bool, switchContext bool, log log.Logger) (kubectl.Client, error) {
145-
kubeLoader := f.NewKubeConfigLoader()
146-
return kubectl.NewClientBySelect(allowPrivate, switchContext, kubeLoader, log)
147-
}
148-
149142
// NewHelmClient implements interface
150143
func (f *DefaultFactoryImpl) NewHelmClient(log log.Logger) (types.Client, error) {
151144
return helm.NewClient(log)

pkg/util/factory/testing/factory.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,6 @@ func (f *Factory) NewKubeClientFromContext(context, namespace string) (kubectl.C
112112
return f.KubeClient, nil
113113
}
114114

115-
// NewKubeClientBySelect implements interface
116-
func (f *Factory) NewKubeClientBySelect(allowPrivate bool, switchContext bool, log log.Logger) (kubectl.Client, error) {
117-
return f.KubeClient, nil
118-
}
119-
120115
// NewHelmClient implements interface
121116
func (f *Factory) NewHelmClient(log log.Logger) (types.Client, error) {
122117
return f.HelmClient, nil

0 commit comments

Comments
 (0)