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 }