-
Notifications
You must be signed in to change notification settings - Fork 163
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
user
property to run configurations (#2055)
* If not set, use the default user from the image (if it, in turn, is not set either, Docker uses `root` as a default value) * The container user is still set to `root`, as we need root privileges, at least to install sshd, but the runner executes the job (shell script with `commands` from the run configuration) as `user`. * If the `user` is not root, it gets its own copy of `~/.ssh/authorized_keys` and `~/.ssh/environment`, making it possible to `ssh user@run-name` (the default user is still `root`, that is, `ssh run-name` logs in as root) * `~/.ssh/environment` is now generated by the runner, not the outer shell script (container entrypoint), and includes all the same variables as the job env (including `DSTACK_*` vars and vars from the `env` property of the run configuration) Part-of: #1535
- Loading branch information
Showing
23 changed files
with
872 additions
and
72 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
package executor | ||
|
||
import ( | ||
"fmt" | ||
"strings" | ||
) | ||
|
||
type EnvMap map[string]string | ||
|
||
func (em EnvMap) Get(key string) string { | ||
return em[key] | ||
} | ||
|
||
func (em EnvMap) Update(src map[string]string, interpolate bool) { | ||
for key, value := range src { | ||
if interpolate { | ||
value = interpolateVariables(value, em.Get) | ||
} | ||
em[key] = value | ||
} | ||
} | ||
|
||
func (em EnvMap) Render() []string { | ||
var list []string | ||
for key, value := range em { | ||
list = append(list, fmt.Sprintf("%s=%s", key, value)) | ||
} | ||
return list | ||
} | ||
|
||
func NewEnvMap(sources ...map[string]string) EnvMap { | ||
em := make(EnvMap) | ||
for _, src := range sources { | ||
em.Update(src, false) | ||
} | ||
return em | ||
} | ||
|
||
func ParseEnvList(list []string) EnvMap { | ||
em := make(EnvMap) | ||
for _, item := range list { | ||
parts := strings.SplitN(item, "=", 2) | ||
if len(parts) == 2 { | ||
em[parts[0]] = parts[1] | ||
} | ||
} | ||
return em | ||
} | ||
|
||
// interpolateVariables expands variables as follows: | ||
// `$VARNAME` -> literal `$VARNAME` (curly brackets are mandatory, bare $ means nothing) | ||
// `${VARNAME}` -> getter("VARNAME") return value | ||
// `$${VARNAME}` -> literal `${VARNAME}` | ||
// `$$${VARNAME}` -> literal `$` + getter("VARNAME") return value | ||
// `$$$${VARNAME}` -> literal `$${VARNAME}` | ||
// `${no_closing_bracket`, `${0nonalphafirstchar}`, `${non-alphanum char}`, `${}` -> | ||
// -> corresponding literal as is (only valid placeholder is treated specially requiring | ||
// doubling $ to avoid interpolation, any non-valid syntax with `${` sequence is passed as is) | ||
// See test cases for more examples | ||
func interpolateVariables(s string, getter func(string) string) string { | ||
// assuming that most strings don't contain vars, | ||
// allocate the buffer the same size as input string | ||
buf := make([]byte, 0, len(s)) | ||
dollarCount := 0 | ||
for i := 0; i < len(s); i++ { | ||
switch char := s[i]; char { | ||
case '$': | ||
dollarCount += 1 | ||
case '{': | ||
name, w := getVariableName(s[i+1:]) | ||
if name != "" { | ||
// valid variable name, unescaping $ | ||
for range dollarCount / 2 { | ||
buf = append(buf, '$') | ||
} | ||
if dollarCount%2 != 0 { | ||
// ${var} -> var_value, $$${var} -> $var_value | ||
buf = append(buf, getter(name)...) | ||
} else { | ||
// $${var} -> ${var}, $$$${var} -> $${var} | ||
buf = append(buf, s[i:i+w+1]...) | ||
} | ||
} else { | ||
// not a valid variable name or unclosed ${}, keeping all $ as is | ||
for range dollarCount { | ||
buf = append(buf, '$') | ||
} | ||
buf = append(buf, s[i:i+w+1]...) | ||
} | ||
i += w | ||
dollarCount = 0 | ||
default: | ||
// flush accumulated $, if any | ||
for range dollarCount { | ||
buf = append(buf, '$') | ||
} | ||
dollarCount = 0 | ||
buf = append(buf, char) | ||
} | ||
} | ||
// flush trailing $, if any | ||
for range dollarCount { | ||
buf = append(buf, '$') | ||
} | ||
return string(buf) | ||
} | ||
|
||
func getVariableName(s string) (string, int) { | ||
if len(s) < 2 { | ||
return "", len(s) | ||
} | ||
if !isAlpha(s[0]) { | ||
return "", 1 | ||
} | ||
var i int | ||
for i = 1; i < len(s); i++ { | ||
char := s[i] | ||
if char == '}' { | ||
return s[:i], i + 1 | ||
} | ||
if !isAlphaNum(char) { | ||
return "", i | ||
} | ||
} | ||
return "", i | ||
} | ||
|
||
func isAlpha(c uint8) bool { | ||
return c == '_' || 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' | ||
} | ||
|
||
func isAlphaNum(c uint8) bool { | ||
return isAlpha(c) || '0' <= c && c <= '9' | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package executor | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func dummyGetter(s string) string { | ||
return "<dummy>" | ||
} | ||
|
||
func TestInterpolateVariables_DollarEscape(t *testing.T) { | ||
testCases := []struct { | ||
input, expected string | ||
}{ | ||
{"", ""}, | ||
{"just a string", "just a string"}, | ||
{"$ $$ $$$", "$ $$ $$$"}, | ||
{"foo $notavar", "foo $notavar"}, | ||
{"foo $$notavar", "foo $$notavar"}, | ||
{"trailing$", "trailing$"}, | ||
{"trailing$$", "trailing$$"}, | ||
{"trailing${", "trailing${"}, | ||
{"trailing$${", "trailing$${"}, | ||
{"empty${}", "empty${}"}, | ||
{"empty${}empty", "empty${}empty"}, | ||
{"empty$${}empty", "empty$${}empty"}, | ||
{"foo${notavar", "foo${notavar"}, | ||
{"foo${notavar bar", "foo${notavar bar"}, | ||
{"foo$${notavar", "foo$${notavar"}, | ||
{"foo$${notavar bar", "foo$${notavar bar"}, | ||
{"foo${!notavar}", "foo${!notavar}"}, | ||
{"foo${!notavar}bar", "foo${!notavar}bar"}, | ||
{"foo${not!a!var}", "foo${not!a!var}"}, | ||
{"foo$${not!a!var}", "foo$${not!a!var}"}, | ||
{"foo${not!a!var}bar", "foo${not!a!var}bar"}, | ||
{"foo$${not!a!var}bar", "foo$${not!a!var}bar"}, | ||
{"${0notavar}", "${0notavar}"}, | ||
{"foo ${0notavar}bar", "foo ${0notavar}bar"}, | ||
{"foo $$${0notavar}bar", "foo $$${0notavar}bar"}, | ||
{"foo$${escaped}", "foo${escaped}"}, | ||
{"foo$$$${escaped}bar", "foo$${escaped}bar"}, | ||
{"${var}", "<dummy>"}, | ||
{"$$${var}", "$<dummy>"}, | ||
{"$$${var}$", "$<dummy>$"}, | ||
{"$$${var}$$", "$<dummy>$$"}, | ||
{"foo${var}bar", "foo<dummy>bar"}, | ||
{"hi ${var_WITH_all_allowed_char_types_013}", "hi <dummy>"}, | ||
} | ||
for _, tc := range testCases { | ||
interpolated := interpolateVariables(tc.input, dummyGetter) | ||
assert.Equal(t, tc.expected, interpolated) | ||
} | ||
} | ||
|
||
func TestEnvMapUpdate_Expand(t *testing.T) { | ||
envMap := EnvMap{"PATH": "/bin:/sbin"} | ||
envMap.Update(EnvMap{"PATH": "/opt/bin:${PATH}"}, true) | ||
assert.Equal(t, EnvMap{"PATH": "/opt/bin:/bin:/sbin"}, envMap) | ||
} | ||
|
||
func TestEnvMapUpdate_Expand_NoCurlyBrackets(t *testing.T) { | ||
envMap := EnvMap{"PATH": "/bin:/sbin"} | ||
envMap.Update(EnvMap{"PATH": "/opt/bin:$PATH"}, true) | ||
assert.Equal(t, EnvMap{"PATH": "/opt/bin:$PATH"}, envMap) | ||
} | ||
|
||
func TestEnvMapUpdate_Expand_MissingVar(t *testing.T) { | ||
envMap := EnvMap{} | ||
envMap.Update(EnvMap{"PATH": "/opt/bin:${PATH}"}, true) | ||
assert.Equal(t, EnvMap{"PATH": "/opt/bin:"}, envMap) | ||
} | ||
|
||
func TestEnvMapUpdate_Expand_VarLike(t *testing.T) { | ||
envMap := EnvMap{} | ||
envMap.Update(EnvMap{"TOKEN": "deadf00d${notavar ${$NOTaVAR}"}, true) | ||
assert.Equal(t, EnvMap{"TOKEN": "deadf00d${notavar ${$NOTaVAR}"}, envMap) | ||
} | ||
|
||
func TestEnvMapUpdate_Merge_NoExpand(t *testing.T) { | ||
envMap := EnvMap{ | ||
"VAR1": "var1_oldvalue", | ||
"VAR2": "var2_value", | ||
} | ||
envMap.Update(map[string]string{ | ||
"VAR1": "var1_newvalue", | ||
"VAR3": "var3_${VAR2}", | ||
}, false) | ||
|
||
expected := EnvMap{ | ||
"VAR1": "var1_newvalue", | ||
"VAR2": "var2_value", | ||
"VAR3": "var3_${VAR2}", | ||
} | ||
assert.Equal(t, expected, envMap) | ||
} | ||
|
||
func TestEnvMapUpdate_Merge_Expand(t *testing.T) { | ||
envMap := EnvMap{ | ||
"VAR1": "var1_oldvalue", | ||
"VAR2": "var2_value", | ||
} | ||
envMap.Update(map[string]string{ | ||
"VAR1": "var1_newvalue", | ||
"VAR3": "var3_${VAR2}", | ||
}, true) | ||
|
||
expected := EnvMap{ | ||
"VAR1": "var1_newvalue", | ||
"VAR2": "var2_value", | ||
"VAR3": "var3_var2_value", | ||
} | ||
assert.Equal(t, expected, envMap) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.