Skip to content

Commit

Permalink
Make okteto up wait until original resource is awaken before starting…
Browse files Browse the repository at this point in the history
… dev container (#3368)

* Changes in the up command to wake the namespace up (if it is sleeping and it is an okteto context) and wait until the annotation dev.okteto.com/state-before-sleeping is not present in the original deployment before deploying the dev container

Signed-off-by: Ignacio Fuertes <nacho@okteto.com>

* Execute wake call within a goroutine, extracted status label to a constant and include some logic to wait the the deployment is already in dev mode but the namespace is sleeping

Signed-off-by: Ignacio Fuertes <nacho@okteto.com>

* If the dev container is an autocreate, we don't need to wait until it is up

Signed-off-by: Ignacio Fuertes <nacho@okteto.com>

* Addressed code review comments. Some renames, stop timers properly and print a warning instead of returning an error if the resource is not awaken after the timeout

Signed-off-by: Ignacio Fuertes <nacho@okteto.com>

---------

Signed-off-by: Ignacio Fuertes <nacho@okteto.com>
  • Loading branch information
ifbyol committed Feb 6, 2023
1 parent ba3387c commit 6f1ffb2
Show file tree
Hide file tree
Showing 17 changed files with 327 additions and 5 deletions.
2 changes: 1 addition & 1 deletion cmd/destroy/destroy.go
Expand Up @@ -539,7 +539,7 @@ func (pc *destroyCommand) waitForNamespaceDestroyAllToComplete(ctx context.Conte
return err
}

status, ok := ns.Labels["space.okteto.com/status"]
status, ok := ns.Labels[constants.NamespaceStatusLabel]
if !ok {
// when status label is not present, continue polling the namespace until timeout
oktetoLog.Debugf("namespace %q does not have label for status", namespace)
Expand Down
3 changes: 2 additions & 1 deletion cmd/namespace/delete.go
Expand Up @@ -24,6 +24,7 @@ import (
contextCMD "github.com/okteto/okteto/cmd/context"
"github.com/okteto/okteto/cmd/utils"
"github.com/okteto/okteto/pkg/analytics"
"github.com/okteto/okteto/pkg/constants"
oktetoErrors "github.com/okteto/okteto/pkg/errors"
oktetoLog "github.com/okteto/okteto/pkg/log"
"github.com/okteto/okteto/pkg/okteto"
Expand Down Expand Up @@ -159,7 +160,7 @@ func (nc *NamespaceCommand) waitForNamespaceDeleted(ctx context.Context, namespa
return err
}

status, ok := ns.Labels["space.okteto.com/status"]
status, ok := ns.Labels[constants.NamespaceStatusLabel]
if !ok {
// when status label is not present, continue polling the namespace until timeout
oktetoLog.Debugf("namespace %q does not have label for status", namespace)
Expand Down
3 changes: 2 additions & 1 deletion cmd/namespace/delete_test.go
Expand Up @@ -18,6 +18,7 @@ import (
"testing"

"github.com/okteto/okteto/internal/test/client"
"github.com/okteto/okteto/pkg/constants"
"github.com/okteto/okteto/pkg/k8s/ingresses"
"github.com/okteto/okteto/pkg/okteto"
"github.com/okteto/okteto/pkg/types"
Expand Down Expand Up @@ -133,7 +134,7 @@ func Test_deleteNamespace(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: currentNamespace,
Labels: map[string]string{
"space.okteto.com/status": "DeleteFailed",
constants.NamespaceStatusLabel: "DeleteFailed",
},
},
}),
Expand Down
3 changes: 2 additions & 1 deletion cmd/preview/destroy.go
Expand Up @@ -24,6 +24,7 @@ import (
contextCMD "github.com/okteto/okteto/cmd/context"
"github.com/okteto/okteto/cmd/utils"
"github.com/okteto/okteto/pkg/analytics"
"github.com/okteto/okteto/pkg/constants"
oktetoErrors "github.com/okteto/okteto/pkg/errors"
oktetoLog "github.com/okteto/okteto/pkg/log"
"github.com/okteto/okteto/pkg/model"
Expand Down Expand Up @@ -173,7 +174,7 @@ func (c destroyPreviewCommand) waitForPreviewDestroyed(ctx context.Context, prev
return err
}

status, ok := ns.Labels["space.okteto.com/status"]
status, ok := ns.Labels[constants.NamespaceStatusLabel]
if !ok {
// when status label is not present, continue polling the namespace until timeout
oktetoLog.Debugf("namespace %q does not have label for status", preview)
Expand Down
3 changes: 2 additions & 1 deletion cmd/preview/destroy_test.go
Expand Up @@ -5,6 +5,7 @@ import (
"testing"

"github.com/okteto/okteto/internal/test/client"
"github.com/okteto/okteto/pkg/constants"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
Expand Down Expand Up @@ -59,7 +60,7 @@ func TestExecuteDestroyPreviewWithFailedJob(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Name: "test-preview",
Labels: map[string]string{
"space.okteto.com/status": "DeleteFailed",
constants.NamespaceStatusLabel: "DeleteFailed",
},
},
}
Expand Down
53 changes: 53 additions & 0 deletions cmd/up/activate.go
Expand Up @@ -118,6 +118,11 @@ func (up *upContext) activate() error {
lastPodUID = up.Pod.UID
}

if err := up.waitUntilAppIsAwaken(ctx, app); err != nil {
oktetoLog.Infof("error waiting for the original %s to be awaken: %s", app.Kind(), err.Error())
return err
}

if err := up.devMode(ctx, app, create); err != nil {
if oktetoErrors.IsTransient(err) {
return err
Expand Down Expand Up @@ -460,3 +465,51 @@ func getPullingMessage(message, namespace string) string {
toReplace := fmt.Sprintf("%s/%s", registry, namespace)
return strings.Replace(message, toReplace, okteto.DevRegistry, 1)
}

// waitUntilAppIsAwaken waits until the app is awaken checking if the annotation dev.okteto.com/state-before-sleeping is present in the app resource
func (up *upContext) waitUntilAppIsAwaken(ctx context.Context, app apps.App) error {
// If it is auto create, we don't need to wait for the app to wake up
if up.Dev.Autocreate {
return nil
}

appToCheck := app
// If the app is already in dev mode, we need to check the cloned app to see if it is awaken
if apps.IsDevModeOn(app) {
var err error
appToCheck, err = app.GetDevClone(ctx, up.Client)
if err != nil {
return err
}
}

if _, ok := appToCheck.ObjectMeta().Annotations[model.StateBeforeSleepingAnnontation]; !ok {
return nil
}

timeout := 5 * time.Minute
to := time.NewTicker(timeout)
ticker := time.NewTicker(2 * time.Second)
defer to.Stop()
defer ticker.Stop()
oktetoLog.Spinner(fmt.Sprintf("Dev environment '%s' is sleeping. Waiting for it to wake up...", appToCheck.ObjectMeta().Name))
oktetoLog.StartSpinner()
defer oktetoLog.StopSpinner()
for {
select {
case <-to.C:
// In case of timeout, we just print a warning to avoid the command to fail
oktetoLog.Warning("Dev environment '%s' didn't wake up after %s", appToCheck.ObjectMeta().Name, timeout.String())
return nil
case <-ticker.C:
if err := appToCheck.Refresh(ctx, up.Client); err != nil {
return err
}

// If the app is not sleeping anymore, we are done
if _, ok := appToCheck.ObjectMeta().Annotations[model.StateBeforeSleepingAnnontation]; !ok {
return nil
}
}
}
}
34 changes: 34 additions & 0 deletions cmd/up/up.go
Expand Up @@ -36,6 +36,8 @@ import (
"github.com/okteto/okteto/pkg/constants"
"github.com/okteto/okteto/pkg/devenvironment"
"github.com/okteto/okteto/pkg/discovery"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"

"github.com/okteto/okteto/pkg/cmd/pipeline"
"github.com/okteto/okteto/pkg/config"
Expand Down Expand Up @@ -295,6 +297,22 @@ func Up() *cobra.Command {
up.Dev.Autocreate = true
}

// only if the context is an okteto one, we should verify if the namespace has to be woken up
if okteto.Context().IsOkteto {
// We execute it in a goroutine to not impact the command performance
go func() {
okClient, err := okteto.NewOktetoClient()
if err != nil {
oktetoLog.Infof("failed to create okteto client: '%s'", err.Error())
return
}
if err := wakeNamespaceIfApplies(ctx, up.Dev.Namespace, up.Client, okClient); err != nil {
// If there is an error waking up namespace, we don't want to fail the up command
oktetoLog.Infof("failed to wake up the namespace: %s", err.Error())
}
}()
}

if err := setBuildEnvVars(ctx, oktetoManifest); err != nil {
return err
}
Expand Down Expand Up @@ -909,3 +927,19 @@ func setBuildEnvVars(ctx context.Context, m *model.Manifest) error {
}
return builder.Build(ctx, buildOptions)
}

// wakeNamespaceIfApplies wakes the namespace if it is sleeping
func wakeNamespaceIfApplies(ctx context.Context, ns string, k8sClient kubernetes.Interface, okClient types.OktetoInterface) error {
n, err := k8sClient.CoreV1().Namespaces().Get(ctx, ns, metav1.GetOptions{})
if err != nil {
return err
}

// If the namespace is not sleeping, do nothing
if n.Labels[constants.NamespaceStatusLabel] != constants.NamespaceStatusSleeping {
return nil
}

oktetoLog.Information("Namespace '%s' is sleeping, waking it up...", ns)
return okClient.Namespaces().Wake(ctx, ns)
}
56 changes: 56 additions & 0 deletions cmd/up/up_test.go
Expand Up @@ -19,10 +19,17 @@ import (
"fmt"
"testing"

"github.com/okteto/okteto/internal/test/client"
"github.com/okteto/okteto/pkg/constants"
oktetoErrors "github.com/okteto/okteto/pkg/errors"
"github.com/okteto/okteto/pkg/model"
"github.com/okteto/okteto/pkg/model/forward"
"github.com/okteto/okteto/pkg/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)

func Test_waitUntilExitOrInterrupt(t *testing.T) {
Expand Down Expand Up @@ -343,3 +350,52 @@ func TestCommandAddedToUpOptionsWhenPassedAsFlag(t *testing.T) {
})
}
}

func TestWakeNamespaceIfAppliesWithoutErrors(t *testing.T) {
tests := []struct {
name string
ns v1.Namespace
expectedWakeCalls int
}{
{
name: "wake namespace if it is not sleeping",
ns: v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Labels: map[string]string{
constants.NamespaceStatusLabel: "Active",
},
},
},
expectedWakeCalls: 0,
},
{
name: "wake namespace if it is sleeping",
ns: v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Labels: map[string]string{
constants.NamespaceStatusLabel: constants.NamespaceStatusSleeping,
},
},
},
expectedWakeCalls: 1,
},
}
ctx := context.Background()

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
k8sClient := fake.NewSimpleClientset(&tt.ns)
nsClient := client.NewFakeNamespaceClient([]types.Namespace{}, nil)
oktetoClient := &client.FakeOktetoClient{
Namespace: nsClient,
}

err := wakeNamespaceIfApplies(ctx, tt.ns.Name, k8sClient, oktetoClient)

require.NoError(t, err)
require.Equal(t, tt.expectedWakeCalls, nsClient.WakeCalls)
})
}
}
13 changes: 13 additions & 0 deletions internal/test/client/namespace.go
Expand Up @@ -22,6 +22,9 @@ import (
type FakeNamespaceClient struct {
namespaces []types.Namespace
err error

// WakeCalls is the number of times Wake was called
WakeCalls int
}

func NewFakeNamespaceClient(ns []types.Namespace, err error) *FakeNamespaceClient {
Expand Down Expand Up @@ -70,3 +73,13 @@ func (*FakeNamespaceClient) Sleep(_ context.Context, _ string) error {
func (*FakeNamespaceClient) DestroyAll(_ context.Context, _ string, _ bool) error {
return nil
}

// Wake wakes up a namespace
func (c *FakeNamespaceClient) Wake(_ context.Context, _ string) error {
if c.err != nil {
return c.err
}

c.WakeCalls++
return nil
}
6 changes: 6 additions & 0 deletions pkg/constants/constants.go
Expand Up @@ -52,4 +52,10 @@ const (

// OktetoEnvFile defines the name for okteto env file
OktetoEnvFile = "OKTETO_ENV"

// NamespaceStatusLabel label added to namespaces to indicate its status
NamespaceStatusLabel = "space.okteto.com/status"

// NamespaceStatusSleeping indicates that the namespace is sleeping
NamespaceStatusSleeping = "Sleeping"
)
10 changes: 10 additions & 0 deletions pkg/k8s/apps/deployments.go
Expand Up @@ -207,3 +207,13 @@ func (i *DeploymentApp) Destroy(ctx context.Context, c kubernetes.Interface) err
func (i *DeploymentApp) PatchAnnotations(ctx context.Context, c kubernetes.Interface) error {
return deployments.PatchAnnotations(ctx, i.d, c)
}

// GetCloned Returns from Kubernetes the cloned deployment
func (i *DeploymentApp) GetDevClone(ctx context.Context, c kubernetes.Interface) (App, error) {
clonedName := model.DevCloneName(i.d.Name)
d, err := deployments.Get(ctx, clonedName, i.d.Namespace, c)
if err == nil {
return NewDeploymentApp(d), nil
}
return nil, err
}
57 changes: 57 additions & 0 deletions pkg/k8s/apps/deployments_test.go
@@ -0,0 +1,57 @@
package apps

import (
"context"
"testing"

"github.com/okteto/okteto/pkg/okteto"
"github.com/stretchr/testify/require"
appsv1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)

func TestDeploymentGetDevCloneWithError(t *testing.T) {
d := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
}
app := DeploymentApp{kind: okteto.Deployment, d: d}
c := fake.NewSimpleClientset()
ctx := context.Background()

_, err := app.GetDevClone(ctx, c)

require.Error(t, err)
}

func TestDeploymentGetDevCloneWithoutError(t *testing.T) {
d := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "test",
},
}

cloned := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: "test-okteto",
Namespace: "test",
Labels: map[string]string{
"dev.okteto.com/clone": "true",
},
},
}

app := DeploymentApp{kind: okteto.Deployment, d: d}
c := fake.NewSimpleClientset(cloned)
ctx := context.Background()
expected := &DeploymentApp{kind: okteto.Deployment, d: cloned}

result, err := app.GetDevClone(ctx, c)

require.NoError(t, err)
require.Equal(t, expected, result)
}
3 changes: 3 additions & 0 deletions pkg/k8s/apps/interface.go
Expand Up @@ -30,6 +30,7 @@ type App interface {
TemplateObjectMeta() metav1.ObjectMeta
PodSpec() *apiv1.PodSpec

// DevClone() creates in memory a clone of the app for dev mode
DevClone() App

CheckConditionErrors(dev *model.Dev) error
Expand All @@ -38,6 +39,8 @@ type App interface {
// TODO: remove after people move to CLI >= 1.14
RestoreOriginal() error

// GetDevClone returns the cloned app from Kubernetes
GetDevClone(ctx context.Context, c kubernetes.Interface) (App, error)
Refresh(ctx context.Context, c kubernetes.Interface) error
Watch(ctx context.Context, result chan error, c kubernetes.Interface)
Deploy(ctx context.Context, c kubernetes.Interface) error
Expand Down

0 comments on commit 6f1ffb2

Please sign in to comment.