github.com/dnephin/dobi@v0.15.0/execenv/environment.go (about)

     1  package execenv
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/dnephin/dobi/logging"
    13  	git "github.com/gogits/git-module"
    14  	"github.com/metakeule/fmtdate"
    15  	"github.com/pkg/errors"
    16  	fasttmpl "github.com/valyala/fasttemplate"
    17  )
    18  
    19  const (
    20  	startTag     = "{"
    21  	endTag       = "}"
    22  	execIDEnvVar = "DOBI_EXEC_ID"
    23  )
    24  
    25  // ExecEnv is a data object which contains variables for an ExecuteContext
    26  type ExecEnv struct {
    27  	ExecID     string
    28  	Project    string
    29  	tmplCache  map[string]string
    30  	workingDir string
    31  	startTime  time.Time
    32  }
    33  
    34  // Unique returns a unique id for this execution
    35  func (e *ExecEnv) Unique() string {
    36  	return e.Project + "-" + e.ExecID
    37  }
    38  
    39  // Resolve template variables to a string value and cache the value
    40  func (e *ExecEnv) Resolve(tmpl string) (string, error) {
    41  	if val, ok := e.tmplCache[tmpl]; ok {
    42  		return val, nil
    43  	}
    44  
    45  	template, err := fasttmpl.NewTemplate(tmpl, startTag, endTag)
    46  	if err != nil {
    47  		return "", err
    48  	}
    49  
    50  	buff := &bytes.Buffer{}
    51  	_, err = template.ExecuteFunc(buff, e.templateContext)
    52  	if err == nil {
    53  		e.tmplCache[tmpl] = buff.String()
    54  	}
    55  	return buff.String(), err
    56  }
    57  
    58  // ResolveSlice resolves all strings in the slice
    59  func (e *ExecEnv) ResolveSlice(tmpls []string) ([]string, error) {
    60  	resolved := []string{}
    61  	for _, tmpl := range tmpls {
    62  		item, err := e.Resolve(tmpl)
    63  		if err != nil {
    64  			return tmpls, err
    65  		}
    66  		resolved = append(resolved, item)
    67  	}
    68  	return resolved, nil
    69  }
    70  
    71  // nolint: gocyclo
    72  func (e *ExecEnv) templateContext(out io.Writer, tag string) (int, error) {
    73  	tag, defValue, hasDefault := splitDefault(tag)
    74  
    75  	write := func(val string, err error) (int, error) {
    76  		if err != nil {
    77  			return 0, err
    78  		}
    79  		if val == "" {
    80  			if !hasDefault {
    81  				return 0, fmt.Errorf("a value is required for variable %q", tag)
    82  			}
    83  			val = defValue
    84  		}
    85  		return out.Write(bytes.NewBufferString(val).Bytes())
    86  	}
    87  
    88  	prefix, suffix := splitPrefix(tag)
    89  	switch prefix {
    90  	case "env":
    91  		return write(os.Getenv(suffix), nil)
    92  	case "git":
    93  		return valueFromGit(out, e.workingDir, suffix, defValue)
    94  	case "time":
    95  		return write(fmtdate.Format(suffix, e.startTime), nil)
    96  	case "fs":
    97  		val, err := valueFromFilesystem(suffix, e.workingDir)
    98  		return write(val, err)
    99  	case "user":
   100  		val, err := valueFromUser(suffix)
   101  		return write(val, err)
   102  	}
   103  
   104  	switch tag {
   105  	case "unique":
   106  		return write(e.Unique(), nil)
   107  	case "project":
   108  		return write(e.Project, nil)
   109  	case "exec-id":
   110  		return write(e.ExecID, nil)
   111  	default:
   112  		return 0, errors.Errorf("unknown variable %q", tag)
   113  	}
   114  }
   115  
   116  // valueFromFilesystem can return either `cwd` or `projectdir`
   117  func valueFromFilesystem(name string, workingdir string) (string, error) {
   118  	switch name {
   119  	case "cwd":
   120  		return os.Getwd()
   121  	case "projectdir":
   122  		return workingdir, nil
   123  	default:
   124  		return "", errors.Errorf("unknown variable \"fs.%s\"", name)
   125  	}
   126  }
   127  
   128  // nolint: gocyclo
   129  func valueFromGit(out io.Writer, cwd string, tag, defValue string) (int, error) {
   130  	writeValue := func(value string) (int, error) {
   131  		return out.Write(bytes.NewBufferString(value).Bytes())
   132  	}
   133  
   134  	writeError := func(err error) (int, error) {
   135  		if defValue == "" {
   136  			return 0, fmt.Errorf("failed resolving variable {git.%s}: %s", tag, err)
   137  		}
   138  
   139  		logging.Log.Warnf("Failed to get variable \"git.%s\", using default", tag)
   140  		return writeValue(defValue)
   141  	}
   142  
   143  	repo, err := git.OpenRepository(cwd)
   144  	if err != nil {
   145  		return writeError(err)
   146  	}
   147  
   148  	switch tag {
   149  	case "branch":
   150  		branch, err := repo.GetHEADBranch()
   151  		if err != nil {
   152  			return writeError(err)
   153  		}
   154  		return writeValue(branch.Name)
   155  	case "sha":
   156  		commit, err := repo.GetCommit("HEAD")
   157  		if err != nil {
   158  			return writeError(err)
   159  		}
   160  		return writeValue(commit.ID.String())
   161  	case "short-sha":
   162  		commit, err := repo.GetCommit("HEAD")
   163  		if err != nil {
   164  			return writeError(err)
   165  		}
   166  		return writeValue(commit.ID.String()[:10])
   167  	default:
   168  		return 0, errors.Errorf("unknown variable \"git.%s\"", tag)
   169  	}
   170  }
   171  
   172  func splitDefault(tag string) (string, string, bool) {
   173  	parts := strings.Split(tag, ":")
   174  	if len(parts) == 1 {
   175  		return tag, "", false
   176  	}
   177  	last := len(parts) - 1
   178  	return strings.Join(parts[:last], ":"), parts[last], true
   179  }
   180  
   181  func splitPrefix(tag string) (string, string) {
   182  	index := strings.Index(tag, ".")
   183  	switch index {
   184  	case -1, 0, len(tag) - 1:
   185  		return "", tag
   186  	default:
   187  		return tag[:index], tag[index+1:]
   188  	}
   189  }
   190  
   191  // NewExecEnvFromConfig returns a new ExecEnv from a Config
   192  func NewExecEnvFromConfig(execID, project, workingDir string) (*ExecEnv, error) {
   193  	env := NewExecEnv(defaultExecID(), getProjectName(project, workingDir), workingDir)
   194  	var err error
   195  	env.ExecID, err = getExecID(execID, env)
   196  	return env, err
   197  }
   198  
   199  // NewExecEnv returns a new ExecEnv from values
   200  func NewExecEnv(execID, project, workingDir string) *ExecEnv {
   201  	return &ExecEnv{
   202  		ExecID:     execID,
   203  		Project:    project,
   204  		tmplCache:  make(map[string]string),
   205  		startTime:  time.Now(),
   206  		workingDir: workingDir,
   207  	}
   208  }
   209  
   210  func getProjectName(project, workingDir string) string {
   211  	if project != "" {
   212  		return project
   213  	}
   214  	project = filepath.Base(workingDir)
   215  	logging.Log.Warnf("meta.project is not set. Using default %q.", project)
   216  	return project
   217  }
   218  
   219  func getExecID(execID string, env *ExecEnv) (string, error) {
   220  	var err error
   221  
   222  	if value, exists := os.LookupEnv(execIDEnvVar); exists {
   223  		return validateExecID(value)
   224  	}
   225  	if execID == "" {
   226  		return env.ExecID, nil
   227  	}
   228  
   229  	execID, err = env.Resolve(execID)
   230  	if err != nil {
   231  		return "", err
   232  	}
   233  	return validateExecID(execID)
   234  }
   235  
   236  func validateExecID(output string) (string, error) {
   237  	output = strings.TrimSpace(output)
   238  
   239  	if output == "" {
   240  		return "", fmt.Errorf("exec-id template was empty after rendering")
   241  	}
   242  	lines := len(strings.Split(output, "\n"))
   243  	if lines > 1 {
   244  		return "", fmt.Errorf(
   245  			"exec-id template rendered to %v lines, expected only one", lines)
   246  	}
   247  
   248  	return output, nil
   249  }
   250  
   251  func defaultExecID() string {
   252  	username, err := getUserName()
   253  	if err == nil {
   254  		return username
   255  	}
   256  	return os.Getenv("USER")
   257  }