Skip to content

Commit

Permalink
feat: add dettached mode (#2538)
Browse files Browse the repository at this point in the history
* feat: add dettached mode

Signed-off-by: Javier López Barba <javier@okteto.com>

* fix: tests

Signed-off-by: Javier López Barba <javier@okteto.com>

* fix: address comments

Signed-off-by: Javier López Barba <javier@okteto.com>

* fix: unit tests

Signed-off-by: Javier López Barba <javier@okteto.com>
  • Loading branch information
jLopezbarb committed Apr 13, 2022
1 parent 8c0ee5c commit b1653c0
Show file tree
Hide file tree
Showing 15 changed files with 936 additions and 67 deletions.
6 changes: 5 additions & 1 deletion cmd/up/activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,11 @@ func (up *upContext) activate() error {
printDisplayContext(up.Dev, divertURL)
durationActivateUp := time.Since(up.StartTime)
analytics.TrackDurationActivateUp(durationActivateUp)
up.CommandResult <- up.runCommand(ctx, up.Dev.Command.Values)
if up.Options.Detach {
up.CommandResult <- up.showDetachedLogs(ctx)
} else {
up.CommandResult <- up.runCommand(ctx, up.Dev.Command.Values)
}
}()

prevError := up.waitUntilExitOrInterruptOrApply(ctx)
Expand Down
142 changes: 142 additions & 0 deletions cmd/up/logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2022 The Okteto Authors
// 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 up

import (
"context"
"encoding/json"
"fmt"
"os"
"os/signal"
"regexp"
"text/template"
"time"

"github.com/fatih/color"
"github.com/okteto/okteto/pkg/config"
oktetoErrors "github.com/okteto/okteto/pkg/errors"
oktetoLog "github.com/okteto/okteto/pkg/log"
"github.com/okteto/okteto/pkg/okteto"
"github.com/pkg/errors"
"github.com/stern/stern/stern"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/client-go/tools/clientcmd"
)

var tempKubeConfigTemplate = "%s/.okteto/kubeconfig-%s"

func (up *upContext) showDetachedLogs(ctx context.Context) error {
tmpKubeconfig, err := createTempKubeconfig(up.Manifest.Name)
if err != nil {
return err
}
defer os.Remove(tmpKubeconfig)

c, err := getSternConfig(tmpKubeconfig)
if err != nil {
return err
}
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
exit := make(chan error, 1)
go func() {
if err := stern.Run(ctx, c); err != nil {
exit <- oktetoErrors.UserError{
E: fmt.Errorf("failed to get logs: %w", err),
}
}
}()
select {
case <-stop:
oktetoLog.Infof("CTRL+C received, starting shutdown sequence")
return oktetoErrors.ErrIntSig
case err := <-exit:
if err != nil {
oktetoLog.Infof("exit signal received due to error: %s", err)
return err
}
}
return nil
}

func createTempKubeconfig(name string) (string, error) {
cfg := okteto.Context().Cfg
destKubeconfigFile := fmt.Sprintf(tempKubeConfigTemplate, config.GetUserHomeDir(), name)
if err := clientcmd.WriteToFile(*cfg, destKubeconfigFile); err != nil {
oktetoLog.Errorf("could not modify the k8s config: %s", err)
return "", err
}
return destKubeconfigFile, nil
}

func getSternConfig(kubeconfigPath string) (*stern.Config, error) {
labelSelector, err := labels.Parse("detached.dev.okteto.com")
if err != nil {
return nil, fmt.Errorf("failed to parse selector as label selector: %s", err.Error())
}
pod, err := regexp.Compile("")
if err != nil {
return nil, fmt.Errorf("failed to compile regular expression from query: %w", err)
}
container, err := regexp.Compile(".*")
if err != nil {
return nil, errors.Wrap(err, "failed to compile regular expression for container query")
}
var tailLines *int64

funs := template.FuncMap{
"json": func(in interface{}) (string, error) {
b, err := json.Marshal(in)
if err != nil {
return "", err
}
return string(b), nil
},
"parseJSON": func(text string) (map[string]interface{}, error) {
obj := make(map[string]interface{})
if err := json.Unmarshal([]byte(text), &obj); err != nil {
return obj, err
}
return obj, nil
},
"color": func(color color.Color, text string) string {
return color.SprintFunc()(text)
},
}
t := "{{color .PodColor .PodName}} {{color .ContainerColor .ContainerName}} {{.Message}}\n"

tmpl, err := template.New("logs").Funcs(funs).Parse(t)
if err != nil {
return nil, err
}
return &stern.Config{
KubeConfig: kubeconfigPath,
ContextName: okteto.UrlToKubernetesContext(okteto.Context().Name),
Namespaces: []string{okteto.Context().Namespace},
PodQuery: pod,
ContainerQuery: container,
InitContainers: true,
EphemeralContainers: true,
TailLines: tailLines,
Since: 48 * time.Hour,
LabelSelector: labelSelector,
FieldSelector: fields.Everything(),
AllNamespaces: false,
ErrOut: os.Stderr,
Out: os.Stdout,
ContainerStates: []stern.ContainerState{"running"},
Template: tmpl,
}, nil
}
53 changes: 44 additions & 9 deletions cmd/up/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,19 @@ import (
// ReconnectingMessage is the message shown when we are trying to reconnect
const ReconnectingMessage = "Trying to reconnect to your cluster. File synchronization will automatically resume when the connection improves."

// UpOptions represents the options available on up command
type UpOptions struct {
DevPath string
Namespace string
K8sContext string
DevName string
Devs []string
Remote int
Deploy bool
Build bool
ForcePull bool
Reset bool
Detach bool
}

// Up starts a development container
Expand All @@ -72,6 +76,10 @@ func Up() *cobra.Command {
return oktetoErrors.ErrNotInDevContainer
}

if err := upOptions.AddArgs(cmd, args); err != nil {
return err
}

u := utils.UpgradeAvailable()
if len(u) > 0 {
warningFolder := filepath.Join(config.GetOktetoHome(), ".warnings")
Expand Down Expand Up @@ -116,10 +124,7 @@ func Up() *cobra.Command {
oktetoManifest.Name = utils.InferName(wd)
}
os.Setenv(model.OktetoNameEnvVar, oktetoManifest.Name)
devName := ""
if len(args) == 1 {
devName = args[0]
}

if len(oktetoManifest.Dev) == 0 {
oktetoLog.Warning("okteto manifest has no 'dev' section.")
answer, err := utils.AskYesNo("Do you want to configure okteto manifest now? [y/n]")
Expand Down Expand Up @@ -162,10 +167,19 @@ func Up() *cobra.Command {
}
}
}
dev, err := utils.GetDevFromManifest(oktetoManifest, devName)
if err != nil {
return err
var dev *model.Dev
if upOptions.Detach {
dev, err = utils.GetDevDetachMode(oktetoManifest, upOptions.Devs)
if err != nil {
return err
}
} else {
dev, err = utils.GetDevFromManifest(oktetoManifest, upOptions.DevName)
if err != nil {
return err
}
}

if err := setBuildEnvVars(oktetoManifest, dev.Name); err != nil {
return err
}
Expand Down Expand Up @@ -241,7 +255,7 @@ func Up() *cobra.Command {
if err != nil && oktetoErrors.ErrManifestFoundButNoDeployCommands != err {
return err
}
if oktetoErrors.ErrManifestFoundButNoDeployCommands != err {
if oktetoErrors.ErrManifestFoundButNoDeployCommands != err && !upOptions.Detach {
up.Dev.Autocreate = false
}
if err != nil {
Expand Down Expand Up @@ -290,9 +304,30 @@ func Up() *cobra.Command {
cmd.Flags().BoolVarP(&upOptions.ForcePull, "pull", "", false, "force dev image pull")
cmd.Flags().MarkHidden("pull")
cmd.Flags().BoolVarP(&upOptions.Reset, "reset", "", false, "reset the file synchronization database")
cmd.Flags().BoolVarP(&upOptions.Detach, "detach", "", false, "activate one more development containers in detached mode")
return cmd
}

// AddArgs sets the args as options and return err if it's not compatible
func (o *UpOptions) AddArgs(cmd *cobra.Command, args []string) error {
if o.Detach {
o.Devs = args
} else {
maxV1Args := 1
docsURL := "https://okteto.com/docs/reference/cli/#up"
if len(args) > maxV1Args {
cmd.Help()
return oktetoErrors.UserError{
E: fmt.Errorf("%q accepts at most %d arg(s), but received %d", cmd.CommandPath(), maxV1Args, len(args)),
Hint: fmt.Sprintf("Visit %s for more information.", docsURL),
}
} else if len(args) == 1 {
o.DevName = args[0]
}
}
return nil
}

func LoadManifestWithInit(ctx context.Context, k8sContext, namespace, devPath string) (*model.Manifest, error) {
dir, err := os.Getwd()
if err != nil {
Expand Down Expand Up @@ -697,7 +732,7 @@ func setBuildEnvVars(m *model.Manifest, devName string) error {
}

var err error
if m.Dev[devName].Image != nil {
if _, ok := m.Dev[devName]; ok && m.Dev[devName].Image != nil {
m.Dev[devName].Image.Name, err = model.ExpandEnv(m.Dev[devName].Image.Name, false)
}

Expand Down
100 changes: 100 additions & 0 deletions cmd/utils/dev.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const (
//DefaultManifest default okteto manifest file
DefaultManifest = "okteto.yml"
secondaryManifest = "okteto.yaml"
detachModePodName = "okteto-dev-env"
)

func LoadManifestContext(devPath string) (*model.ContextResource, error) {
Expand Down Expand Up @@ -204,6 +205,105 @@ func GetDevFromManifest(manifest *model.Manifest, devName string) (*model.Dev, e
return manifest.Dev[devKey], nil
}

// GetDevDetachMode returns a dev manifest from a
func GetDevDetachMode(manifest *model.Manifest, devs []string) (*model.Dev, error) {
dev := model.NewDev()
dev.Autocreate = true

if manifest.Type == model.StackType {
for svcName, svc := range manifest.Deploy.ComposeSection.Stack.Services {
d, err := svc.ToDev(svcName)
if err != nil {
return nil, err
}
for _, forward := range d.Forward {
localPort := forward.Local
if !model.IsPortAvailable(dev.Interface, forward.Local) {
localPort, err = model.GetAvailablePort(dev.Interface)
if err != nil {
return nil, err
}
}
dev.Forward = append(dev.Forward, model.Forward{
Local: localPort,
Remote: forward.Remote,
ServiceName: svcName,
Service: true,
})
}
if len(d.Sync.Folders) == 0 {
continue
}
if len(devs) > 0 && !isInDevs(svcName, devs) {
continue
}
for _, f := range d.Sync.Folders {
mountValue := filepath.Join("/", d.Name, f.RemotePath)
dev.Sync.Folders = append(dev.Sync.Folders, model.SyncFolder{
LocalPath: f.LocalPath,
RemotePath: mountValue,
})
f.LocalPath = mountValue
}
dev.Services = append(dev.Services, d)
}
} else {
var err error
for dName, d := range manifest.Dev {
for _, forward := range d.Forward {
localPort := forward.Local
if !model.IsPortAvailable(dev.Interface, forward.Local) {
localPort, err = model.GetAvailablePort(dev.Interface)
if err != nil {
return nil, err
}
}
dev.Forward = append(dev.Forward, model.Forward{
Local: localPort,
Remote: forward.Remote,
ServiceName: d.Name,
Service: true,
})
}
if len(devs) > 0 && !isInDevs(dName, devs) {
continue
}
dev.Services = append(dev.Services, d)
for _, f := range d.Sync.Folders {
mountValue := filepath.Join("/", d.Name, f.RemotePath)
dev.Sync.Folders = append(dev.Sync.Folders, model.SyncFolder{
LocalPath: f.LocalPath,
RemotePath: mountValue,
})
f.LocalPath = mountValue
}

}
}
if err := dev.SetDefaults(); err != nil {
return nil, err
}
for _, d := range dev.Services {
if err := d.SetDefaults(); err != nil {
return nil, err
}
}
dev.Name = detachModePodName
dev.Namespace = okteto.Context().Namespace
dev.Context = okteto.Context().Name

return dev, nil
}

func isInDevs(svc string, devs []string) bool {
for _, d := range devs {
if svc == d {
return true
}
}
return false
}

//AskYesNo prompts for yes/no confirmation
func AskYesNo(q string) (bool, error) {
var answer string
Expand Down

0 comments on commit b1653c0

Please sign in to comment.