github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/sync/sync.go (about)

     1  package sync
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/olli-ai/jx/v2/pkg/ksync"
    13  	"github.com/pkg/errors"
    14  
    15  	"github.com/olli-ai/jx/v2/pkg/cmd/helper"
    16  
    17  	"github.com/spf13/cobra"
    18  	"k8s.io/client-go/kubernetes"
    19  
    20  	"github.com/jenkins-x/jx-logging/pkg/log"
    21  	"github.com/olli-ai/jx/v2/pkg/cmd/opts"
    22  	"github.com/olli-ai/jx/v2/pkg/cmd/templates"
    23  	"github.com/olli-ai/jx/v2/pkg/kube"
    24  	"github.com/olli-ai/jx/v2/pkg/util"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  )
    27  
    28  type SyncOptions struct {
    29  	*opts.CommonOptions
    30  
    31  	Daemon      bool
    32  	NoKsyncInit bool
    33  	SingleMode  bool
    34  
    35  	Container string
    36  	Namespace string
    37  	Pod       string
    38  	Dir       string
    39  	RemoteDir string
    40  	Reload    bool
    41  	WatchOnly bool
    42  }
    43  
    44  var (
    45  	sync_long = templates.LongDesc(`
    46  		Synchronises your local files to a DevPod so you an build and test your code easily on the cloud
    47  
    48  		For more documentation see: [https://jenkins-x.io/developing/devpods/](https://jenkins-x.io/developing/devpods/)
    49  
    50  `)
    51  
    52  	sync_example = templates.Examples(`
    53  		# Starts synchronizing the current directory files to the users DevPod
    54  		jx sync 
    55  `)
    56  
    57  	defaultStignoreFile = `.git
    58  .idea
    59  .settings
    60  .vscode
    61  bin
    62  build
    63  target
    64  node_modules
    65  `
    66  )
    67  
    68  func NewCmdSync(commonOpts *opts.CommonOptions) *cobra.Command {
    69  	options := &SyncOptions{
    70  		CommonOptions: commonOpts,
    71  	}
    72  	cmd := &cobra.Command{
    73  		Use:     "sync",
    74  		Short:   "Synchronises your local files to a DevPod",
    75  		Long:    sync_long,
    76  		Example: sync_example,
    77  		Aliases: []string{"log"},
    78  		Run: func(cmd *cobra.Command, args []string) {
    79  			options.Cmd = cmd
    80  			options.Args = args
    81  			err := options.Run()
    82  			helper.CheckErr(err)
    83  		},
    84  	}
    85  	/*	cmd.Flags().StringVarP(&options.Container, "container", "c", "", "The name of the container to log")
    86  		cmd.Flags().StringVarP(&options.Namespace, "namespace", "n", "", "the namespace to look for the Deployment. Defaults to the current namespace")
    87  		cmd.Flags().StringVarP(&options.Pod, "pod", "p", "", "the pod name to use")
    88  		cmd.Flags().StringVarP(&options.Dir, "dir", "d", "", "The directory to watch. Defaults to the current directory")
    89  		cmd.Flags().StringVarP(&options.RemoteDir, "remote-dir", "r", "", "The remote directory in the DevPod to sync")
    90  		cmd.Flags().BoolVarP(&options.Reload, "reload", "", false, "Should we reload the remote container on file changes?")
    91  	*/
    92  	cmd.Flags().BoolVarP(&options.Daemon, "daemon", "", false, "Runs ksync in a background daemon")
    93  	cmd.Flags().BoolVarP(&options.NoKsyncInit, "no-init", "", false, "Disables the use of 'ksync init' to ensure we have initialised ksync")
    94  	cmd.Flags().BoolVarP(&options.SingleMode, "single-mode", "", false, "Terminates eagerly if `ksync watch` fails")
    95  
    96  	// deprecated
    97  	cmd.Flags().BoolVarP(&options.WatchOnly, "watch-only", "", false, "Deprecated this flag is now ignored!")
    98  	return cmd
    99  }
   100  
   101  func (o *SyncOptions) Run() error {
   102  
   103  	// ksync is installed to the jx/bin dir, so we can add it for the user
   104  	err := os.Setenv("PATH", util.PathWithBinary())
   105  	if err != nil {
   106  		return errors.Wrap(err, "failed to set PATH env variable")
   107  	}
   108  
   109  	client, err := o.KubeClient()
   110  	if err != nil {
   111  		return err
   112  	}
   113  	sha, err := ksync.InstallKSync()
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	if !o.NoKsyncInit {
   119  		flag, err := kube.IsDaemonSetExists(client, "ksync", "kube-system")
   120  		if !flag || err != nil {
   121  			log.Logger().Infof("Initialising ksync")
   122  			kcli, err := ksync.NewCLI()
   123  			if err != nil {
   124  				return err
   125  			}
   126  			// Deal with https://github.com/ksync/ksync/issues/218
   127  			_, err = kcli.Init("--upgrade", "--image", fmt.Sprintf("ksync/ksync:git-%s", sha))
   128  			if err != nil {
   129  				return err
   130  			}
   131  		}
   132  	}
   133  
   134  	if o.SingleMode {
   135  		return o.KsyncWatch()
   136  	}
   137  	for {
   138  		err = o.KsyncWatch()
   139  		if err != nil {
   140  			log.Logger().Warnf("Failed on ksync watch: %s", err)
   141  		}
   142  	}
   143  }
   144  
   145  func (o *SyncOptions) waitForKsyncWatchToFail() {
   146  	logged := false
   147  	for {
   148  		_, err := o.GetCommandOutput("", "ksync", "get")
   149  		if err != nil {
   150  			// lets assume watch is no longer running
   151  			log.Logger().Infof("Looks like 'ksync watch' is not running: %s", err)
   152  			return
   153  		}
   154  		if !logged {
   155  			logged = true
   156  			log.Logger().Infof("It looks like 'ksync watch' is already running so we don't need to run it yet...")
   157  		}
   158  		time.Sleep(time.Second * 5)
   159  	}
   160  }
   161  
   162  func (o *SyncOptions) KsyncWatch() error {
   163  	o.waitForKsyncWatchToFail()
   164  
   165  	args := []string{"watch"}
   166  	if o.Daemon {
   167  		args = append(args, "--daemon")
   168  	}
   169  	cmd := exec.Command("ksync", args...)
   170  	cmd.Stdout = o.Out
   171  	cmd.Stderr = o.Out
   172  	err := cmd.Start()
   173  	if err != nil {
   174  		return err
   175  	}
   176  
   177  	log.Logger().Infof("Started the ksync watch")
   178  	time.Sleep(1 * time.Second)
   179  
   180  	state := cmd.ProcessState
   181  	if state != nil && state.Exited() {
   182  		return fmt.Errorf("ksync watch terminated")
   183  	}
   184  	return cmd.Wait()
   185  }
   186  
   187  // CreateKsync removes the exiting ksync if it already exists then create a new ksync of the given name
   188  func (o *SyncOptions) CreateKsync(client kubernetes.Interface, ns string, name string, dir string, remoteDir string, username string) error {
   189  
   190  	// ksync is installed to the jx/bin dir, so we can add it for the user
   191  	err := os.Setenv("PATH", util.PathWithBinary())
   192  	if err != nil {
   193  		return errors.Wrap(err, "failed to set PATH env variable")
   194  	}
   195  
   196  	info := util.ColorInfo
   197  	log.Logger().Infof("synchronizing directory %s to DevPod %s path %s", info(dir), info(name), info(remoteDir))
   198  
   199  	ignoreFile := filepath.Join(dir, ".stignore")
   200  	exists, err := util.FileExists(ignoreFile)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	if !exists {
   205  		err = ioutil.WriteFile(ignoreFile, []byte(defaultStignoreFile), util.DefaultWritePermissions)
   206  		if err != nil {
   207  			return err
   208  		}
   209  	}
   210  	matchLabels := map[string]string{
   211  		kube.LabelDevPodUsername: username,
   212  	}
   213  	selector, err := metav1.LabelSelectorAsSelector(&metav1.LabelSelector{MatchLabels: matchLabels})
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	_, pods, err := kube.GetPodsWithLabels(client, ns, selector.String())
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	// ignore the bad lines that come
   224  	ignoreNames := []string{"starting", "watching"}
   225  
   226  	deleteNames := []string{}
   227  	err = o.Retry(5, time.Second, func() error {
   228  		text, err := o.GetCommandOutput(dir, "ksync", "get")
   229  		if err == nil {
   230  			for i, line := range strings.Split(text, "\n") {
   231  				if i > 1 {
   232  					cols := strings.Split(strings.TrimSpace(line), " ")
   233  					if len(cols) > 2 {
   234  						n := strings.TrimSpace(cols[0])
   235  						if n == name || pods[n] == nil {
   236  							if util.StringArrayIndex(deleteNames, n) < 0 && util.StringArrayIndex(ignoreNames, n) < 0 {
   237  								deleteNames = append(deleteNames, n)
   238  							}
   239  						}
   240  					}
   241  				}
   242  			}
   243  		}
   244  		return err
   245  	})
   246  	if err != nil {
   247  		log.Logger().Warnf("Failed to get from ksync daemon: %s", err)
   248  	}
   249  
   250  	reload := "--reload=false"
   251  	if o.Reload {
   252  		reload = "--reload=true"
   253  	}
   254  
   255  	for _, n := range deleteNames {
   256  		// ignore results as we may not have a spec yet for this name
   257  		log.Logger().Infof("Removing old ksync %s", n)
   258  
   259  		err = o.RunCommand("ksync", "delete", n)
   260  		if err != nil {
   261  			return err
   262  		}
   263  	}
   264  
   265  	time.Sleep(1 * time.Second)
   266  
   267  	return o.RunCommand("ksync", "create", "--name", name, "-l", "jenkins.io/devpod="+name, reload, "-n", ns, dir, remoteDir)
   268  }