github.com/mgoltzsche/ctnr@v0.7.1-alpha/cmd/common.go (about)

     1  // Copyright © 2017 Max Goltzsche
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cmd
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"os"
    21  	"path/filepath"
    22  	"runtime/debug"
    23  	"strings"
    24  
    25  	"github.com/mgoltzsche/ctnr/bundle"
    26  	"github.com/mgoltzsche/ctnr/bundle/builder"
    27  	"github.com/mgoltzsche/ctnr/image"
    28  	"github.com/mgoltzsche/ctnr/model"
    29  	"github.com/mgoltzsche/ctnr/model/oci"
    30  	exterrors "github.com/mgoltzsche/ctnr/pkg/errors"
    31  	"github.com/mgoltzsche/ctnr/run"
    32  	"github.com/mgoltzsche/ctnr/run/factory"
    33  	"github.com/pkg/errors"
    34  	"github.com/sirupsen/logrus"
    35  	"github.com/spf13/cobra"
    36  )
    37  
    38  func wrapRun(cf func(cmd *cobra.Command, args []string) error) func(cmd *cobra.Command, args []string) {
    39  	return func(cmd *cobra.Command, args []string) {
    40  		defer func() {
    41  			if err := recover(); err != nil {
    42  				msg := "\n  OUPS, THIS SEEMS TO BE A BUG!"
    43  				msg += "\n  Please report it at"
    44  				msg += "\n    https://github.com/mgoltzsche/ctnr/issues/new"
    45  				msg += "\n  with a description of what you did and the stacktrace"
    46  				msg += "\n  below if you cannot find an already existing issue at"
    47  				msg += "\n    https://github.com/mgoltzsche/ctnr/issues\n"
    48  				stackTrace := strings.Replace(string(debug.Stack()), "\n", "\n  ", -1)
    49  				// TODO: Add version
    50  				logrus.Fatalf("%+v\n%s\n  PANIC: %s\n  %s", err, msg, err, stackTrace)
    51  				os.Exit(255)
    52  			}
    53  		}()
    54  		err := cf(cmd, args)
    55  		closeLockedImageStore()
    56  		exitOnError(cmd, err)
    57  	}
    58  }
    59  
    60  func exitOnError(cmd *cobra.Command, err error) {
    61  	if err == nil {
    62  		return
    63  	}
    64  
    65  	// Usage error - print help text and exit
    66  	if _, ok := err.(UsageError); ok {
    67  		logger.Errorf("%s\n%s\n%s", err, cmd.UsageString(), err)
    68  		os.Exit(1)
    69  	}
    70  
    71  	// Handle exit error
    72  	exitCode := 255
    73  	errLog := loggers.Error
    74  	cause := errors.Cause(err)
    75  	if exitErr, ok := cause.(*run.ExitError); ok {
    76  		exitCode = exitErr.Code()
    77  		errLog = errLog.WithField("id", exitErr.ContainerID()).WithField("code", exitCode)
    78  		err = errors.New("container process terminated with error")
    79  	}
    80  
    81  	// Log stacktrace
    82  	errStr := err.Error()
    83  	errDetails, _ := errorCausesString(err, nil)
    84  	if errDetails != errStr {
    85  		loggers.Debug.Println(errDetails)
    86  	}
    87  
    88  	// Print error & exit
    89  	errLog.Println(errStr)
    90  	os.Exit(exitCode)
    91  }
    92  
    93  func errorCausesString(err error, lastTraceLines []string) (debug string, lastTracedCause error) {
    94  	type causer interface {
    95  		error
    96  		Cause() error
    97  	}
    98  	type tracer interface {
    99  		error
   100  		StackTrace() errors.StackTrace
   101  	}
   102  	str := ""
   103  	if traced, ok := err.(tracer); ok {
   104  		st := traced.StackTrace()
   105  		traceLines := make([]string, len(st))
   106  		for i, t := range st {
   107  			traceLines[i] = fmt.Sprintf("%+v", t)
   108  		}
   109  		truncate := len(traceLines)
   110  		offset := len(traceLines) - len(lastTraceLines)
   111  		for i := len(traceLines) - 1; i >= 0; i-- {
   112  			j := i - offset
   113  			if j < 0 || len(lastTraceLines) <= j || lastTraceLines[j] != traceLines[i] {
   114  				truncate = i + 1
   115  				break
   116  			}
   117  		}
   118  		for _, t := range traceLines[0:truncate] {
   119  			str += "\n    " + strings.Replace(t, "\n", "\n    ", -1)
   120  		}
   121  		if truncate < len(traceLines)-1 {
   122  			str += "\n    ... (truncated)"
   123  		}
   124  		lastTraceLines = traceLines
   125  	}
   126  	errMsg := err.Error()
   127  	wrapper, ok := err.(causer)
   128  	if ok && wrapper.Cause() != nil {
   129  		cause := wrapper.Cause()
   130  		causeStr, lastTracedCause := errorCausesString(cause, lastTraceLines)
   131  		if str == "" {
   132  			return causeStr, lastTracedCause
   133  		}
   134  		causeMsg := ""
   135  		if lastTracedCause != nil {
   136  			causeMsg = lastTracedCause.Error()
   137  		}
   138  		pos := len(errMsg) - len(causeMsg)
   139  		if strings.HasSuffix(errMsg, ": "+causeMsg) && pos > 0 {
   140  			str = errMsg[0:pos-2] + str
   141  		} else {
   142  			str = errMsg + str
   143  		}
   144  		return str + "\n  " + causeStr, err
   145  	}
   146  	return errMsg + str, err
   147  }
   148  
   149  func usageError(msg string) UsageError {
   150  	return UsageError(msg)
   151  }
   152  
   153  type UsageError string
   154  
   155  func (err UsageError) Error() string {
   156  	return string(err)
   157  }
   158  
   159  func exitError(exitCode int, frmt string, values ...interface{}) {
   160  	loggers.Error.Printf(frmt, values...)
   161  	os.Exit(exitCode)
   162  }
   163  
   164  func openImageStore() (image.ImageStoreRW, error) {
   165  	if lockedImageStore == nil {
   166  		s, err := store.OpenLockedImageStore()
   167  		if err != nil {
   168  			return nil, err
   169  		}
   170  		lockedImageStore = s
   171  	}
   172  	return lockedImageStore, nil
   173  }
   174  
   175  func closeLockedImageStore() {
   176  	if lockedImageStore != nil {
   177  		lockedImageStore.Close()
   178  	}
   179  }
   180  
   181  func newContainerManager() (run.ContainerManager, error) {
   182  	return factory.NewContainerManager(filepath.Join(flagStateDir, "containers"), flagRootless, loggers)
   183  }
   184  
   185  func resourceResolver(baseDir string, volumes map[string]model.Volume) model.ResourceResolver {
   186  	paths := model.NewPathResolver(baseDir)
   187  	return model.NewResourceResolver(paths, volumes)
   188  }
   189  
   190  func runServices(services []model.Service, res model.ResourceResolver) (err error) {
   191  	manager, err := newContainerManager()
   192  	if err != nil {
   193  		return
   194  	}
   195  
   196  	containers := run.NewContainerGroup(loggers.Debug)
   197  	defer func() {
   198  		err = exterrors.Append(err, containers.Close())
   199  	}()
   200  
   201  	for _, s := range services {
   202  		var c run.Container
   203  		loggers.Debug.Println(s.JSON())
   204  		if c, err = createContainer(&s, res, manager, true); err != nil {
   205  			return
   206  		}
   207  		containers.Add(c)
   208  	}
   209  
   210  	closeLockedImageStore()
   211  	containers.Start()
   212  	containers.Wait()
   213  	return
   214  }
   215  
   216  func createContainer(model *model.Service, res model.ResourceResolver, manager run.ContainerManager, destroyOnClose bool) (c run.Container, err error) {
   217  	var bundle *bundle.LockedBundle
   218  	if bundle, err = createRuntimeBundle(model, res); err != nil {
   219  		return
   220  	}
   221  	defer func() {
   222  		err = exterrors.Append(err, bundle.Close())
   223  	}()
   224  
   225  	ioe := run.NewStdContainerIO()
   226  	if model.StdinOpen {
   227  		ioe.Stdin = os.Stdin
   228  	}
   229  
   230  	return manager.NewContainer(&run.ContainerConfig{
   231  		Id:             "",
   232  		Bundle:         bundle,
   233  		Io:             ioe,
   234  		NoNewKeyring:   model.NoNewKeyring,
   235  		NoPivotRoot:    model.NoPivot,
   236  		DestroyOnClose: destroyOnClose,
   237  	})
   238  }
   239  
   240  func createRuntimeBundle(service *model.Service, res model.ResourceResolver) (b *bundle.LockedBundle, err error) {
   241  	if service.Image == "" {
   242  		return nil, errors.Errorf("service %q has no image", service.Name)
   243  	}
   244  
   245  	istore, err := openImageStore()
   246  	if err != nil {
   247  		return
   248  	}
   249  
   250  	bundleId := service.Bundle
   251  	bundleDir := ""
   252  	if isFile(bundleId) {
   253  		bundleDir = bundleId
   254  		bundleId = ""
   255  	}
   256  
   257  	// Create bundle
   258  	if bundleDir != "" {
   259  		b, err = bundle.CreateLockedBundle(bundleDir, service.BundleUpdate)
   260  	} else {
   261  		b, err = store.CreateBundle(bundleId, service.BundleUpdate)
   262  	}
   263  	if err != nil {
   264  		return
   265  	}
   266  	defer func() {
   267  		if err != nil {
   268  			b.Delete()
   269  		}
   270  	}()
   271  
   272  	// Apply image
   273  	builder := builder.Builder(b.ID())
   274  	if service.Image != "" {
   275  		var img image.Image
   276  		if img, err = image.GetImage(istore, service.Image); err != nil {
   277  			return b, err
   278  		}
   279  		builder.SetImage(image.NewUnpackableImage(&img, istore))
   280  	}
   281  
   282  	// Apply config.json
   283  	netDataDir := filepath.Join(flagStateDir, "networks")
   284  	if err = oci.ToSpec(service, res, flagRootless, netDataDir, flagPRootPath, builder); err != nil {
   285  		return b, err
   286  	}
   287  
   288  	return b, builder.Build(b)
   289  }
   290  
   291  func isFile(file string) bool {
   292  	return file != "" && (filepath.IsAbs(file) || file == "." || file == ".." || len(file) > 1 && file[0:2] == "./" || len(file) > 2 && file[0:3] == "../" || file == "~" || len(file) > 1 && file[0:2] == "~/")
   293  }
   294  
   295  func checkNonEmpty(s string) (err error) {
   296  	if len(bytes.TrimSpace([]byte(s))) == 0 {
   297  		err = usageError("empty value")
   298  	}
   299  	return
   300  }