Skip to content

Commit

Permalink
Control syntax of file created with the intercept flag --env-file
Browse files Browse the repository at this point in the history
A new `--env-syntax <syntax>` was introduced to allow control over the
syntax of the file created when using the flag `--env-file <file>`.
Valid syntaxes are "docker", "compose", "sh", "csh", "cmd", and "ps";
where "sh", "csh", and "ps" can be suffixed with ":export".

Closes #1720

Signed-off-by: Thomas Hallgren <thomas@tada.se>
Signed-off-by: Thomas Hallgren <thomas@datawire.io>
  • Loading branch information
thallgren committed Aug 25, 2024
1 parent 890ad63 commit d79275f
Show file tree
Hide file tree
Showing 9 changed files with 393 additions and 118 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ items:
can be installed using the command:<br>
<code>helm install traffic-manager oci://ghcr.io/telepresenceio/telepresence-oss --namespace ambassador --version 2.20.0</code>
The chart documentation is published at <a href="https://artifacthub.io/packages/helm/telepresence-oss/telepresence-oss">ArtifactHUB</a>.
- type: feature
title: Control the syntax of the environment file created with the intercept flag --env-file
body: >-
A new <code>--env-syntax &lt;syntax&gt;</code> was introduced to allow control over the syntax of the file created when using the intercept
flag <code>--env-file &lt;file&gt;</code>. Valid syntaxes are &quot;docker&quot;, &quot;compose&quot;, &quot;sh&quot;, &quot;csh&quot;, &quot;cmd&quot;,
and &quot;ps&quot;; where &quot;sh&quot;, &quot;csh&quot;, and &quot;ps&quot; can be suffixed with &quot;:export&quot;.
- type: feature
title: Add support for Argo Rollout workloads.
body: >-
Expand Down
2 changes: 1 addition & 1 deletion build-aux/main.mk
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ BEXE=
BZIP=
endif

EMBED_FUSEFTP=1
EMBED_FUSEFTP=0

# Generate: artifacts that get checked in to Git
# ==============================================
Expand Down
16 changes: 9 additions & 7 deletions pkg/client/cli/intercept/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ type Command struct {

Replace bool // whether --replace was passed

EnvFile string // --env-file
EnvJSON string // --env-json
Mount string // --mount // "true", "false", or desired mount point // only valid if !localOnly
MountSet bool // whether --mount was passed
ToPod []string // --to-pod
EnvFile string // --env-file
EnvSyntax EnvironmentSyntax
EnvJSON string // --env-json
Mount string // --mount // "true", "false", or desired mount point // only valid if !localOnly
MountSet bool // whether --mount was passed
ToPod []string // --to-pod

DockerRun bool // --docker-run
DockerBuild string // --docker-build DIR | URL
Expand Down Expand Up @@ -71,8 +72,9 @@ func (a *Command) AddFlags(cmd *cobra.Command) {
`Declare a local-only intercept for the purpose of getting direct outbound access to the intercept's namespace`)

flagSet.StringVarP(&a.EnvFile, "env-file", "e", "", ``+
`Also emit the remote environment to an env file in Docker Compose format. `+
`See https://docs.docker.com/compose/env-file/ for more information on the limitations of this format.`)
`Also emit the remote environment to an file. The syntax used in the file can be determined using flag --env-syntax`)

flagSet.Var(&a.EnvSyntax, "env-syntax", `Syntax used for env-file. One of `+EnvSyntaxUsage())

flagSet.StringVarP(&a.EnvJSON, "env-json", "j", "", `Also emit the remote environment to a file as a JSON blob.`)

Expand Down
148 changes: 148 additions & 0 deletions pkg/client/cli/intercept/envsyntax.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package intercept

import (
"fmt"
"slices"
"strings"

"github.com/telepresenceio/telepresence/v2/pkg/shellquote"
)

type EnvironmentSyntax int

const (
envSyntaxDocker EnvironmentSyntax = iota
envSyntaxCompose
envSyntaxSh
envSyntaxShExport
envSyntaxCsh
envSyntaxCshExport
envSyntaxPS
envSyntaxPSExport
envSyntaxCmd
)

var envSyntaxNames = []string{ //nolint:gochecknoglobals // constant
"docker",
"compose",
"sh",
"sh:export",
"csh",
"csh:export",
"ps",
"ps:export",
"cmd",
}

func EnvSyntaxUsage() string {
return `"docker", "compose", "sh", "csh", "cmd", and "ps"; where "sh", "csh", and "ps" can be suffixed with ":export"`
}

// Set uses a pointer receiver intentionally, even though the internal type is int, because
// it must change the actual receiver value.
func (e *EnvironmentSyntax) Set(n string) error {
ex := slices.Index(envSyntaxNames, n)
if ex < 0 {
return fmt.Errorf("invalid env syntax: %s", n)
}
*e = EnvironmentSyntax(ex)
return nil
}

func (e EnvironmentSyntax) String() string {
if e >= 0 && e <= envSyntaxCmd {
return envSyntaxNames[e]
}
return "unknown"
}

func (e EnvironmentSyntax) Type() string {
return "string"
}

// WriteEnv will write the environment variable in a form that will make the target shell parse it correctly and verbatim.
func (e EnvironmentSyntax) WriteEnv(k, v string) (r string, err error) {
switch e {
case envSyntaxDocker:
// Docker does not accept multi-line environments
if strings.IndexByte(v, '\n') >= 0 {
return "", fmt.Errorf("docker run/build does not support multi-line environment values: key: %s, value %s", k, v)
}
r = fmt.Sprintf("%s=%s", k, v)
case envSyntaxCompose:
r = fmt.Sprintf("%s=%s", k, quoteCompose(v))
case envSyntaxSh:
r = fmt.Sprintf("%s=%s", k, shellquote.Unix(v))
case envSyntaxShExport:
r = fmt.Sprintf("export %s=%s", k, shellquote.Unix(v))
case envSyntaxCsh:
r = fmt.Sprintf("set %s=%s", k, shellquote.Unix(v))
case envSyntaxCshExport:
r = fmt.Sprintf("setenv %s %s", k, shellquote.Unix(v))
case envSyntaxPS:
r = fmt.Sprintf("$Env:%s=%s", k, quotePS(v))
case envSyntaxPSExport:
r = fmt.Sprintf("[Environment]::SetEnvironmentVariable(%s, %s, 'User')", quotePS(k), quotePS(v))
case envSyntaxCmd:
if strings.IndexByte(v, '\n') >= 0 {
return "", fmt.Errorf("cmd does not support multi-line environment values: key: %s, value %s", k, v)
}
r = fmt.Sprintf("set %s=%s", k, v)
}
return r, nil
}

// quotePS will put single quotes around the given value, which effectively removes all special meanings of
// all contained characters, with one exception. Powershell uses pairs of single quotes to represent one single
// quote in a quoted string.
func quotePS(s string) string {
sb := strings.Builder{}
sb.WriteByte('\'')
for _, c := range s {
if c == '\'' {
sb.WriteByte('\'')
}
sb.WriteRune(c)
}
sb.WriteByte('\'')
return sb.String()
}

// quoteCompose checks if the give string contains characters that have special meaning for
// docker compose. If it does, it will be quoted using either double or single quotes depending
// on whether the string contains newlines, carriage returns, or tabs. Quotes within the value itself will
// be escaped using backslash.
func quoteCompose(s string) string {
if s == "" {
return ``
}
q := byte('\'')
if strings.ContainsAny(s, "\n\t\r") {
q = '"'
} else if !shellquote.UnixEscape.MatchString(s) {
return s
}

sb := strings.Builder{}
sb.WriteByte(q)
for _, c := range s {
switch c {
case rune(q):
sb.WriteByte('\\')
sb.WriteRune(c)
case '\n':
sb.WriteByte('\\')
sb.WriteByte('n')
case '\t':
sb.WriteByte('\\')
sb.WriteByte('t')
case '\r':
sb.WriteByte('\\')
sb.WriteByte('r')
default:
sb.WriteRune(c)
}
}
sb.WriteByte(q)
return sb.String()
}
116 changes: 116 additions & 0 deletions pkg/client/cli/intercept/envsyntax_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package intercept

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestEnvironmentSyntax_WriteEnv(t *testing.T) {
tests := []struct {
name string
e EnvironmentSyntax
key string
value string
want string
}{
{
`sh A=B C`,
envSyntaxSh,
`A`,
`B C`,
`A='B C'`,
},
{
`sh A=B "C"`,
envSyntaxSh,
`A`,
`B "C"`,
`A='B "C"'`,
},
{
`sh A="B C"`,
envSyntaxSh,
`A`,
`"B C"`,
`A='"B C"'`,
},
{
`sh A=B 'C X'`,
envSyntaxSh,
`A`,
`B 'C X'`,
`A='B '\''C X'\'`,
},
{
`compose A=B 'C X'`,
envSyntaxCompose,
`A`,
`B 'C X'`,
`A='B \'C X\''`,
},
{
`compose A=B\nC\t"D"`,
envSyntaxCompose,
`A`,
"B\nC\t\"D\"",
`A="B\nC\t\"D\""`,
},
{
`sh A='B C'`,
envSyntaxSh,
`A`,
`'B C'`,
`A=\''B C'\'`,
},
{
`sh A=\"B\" \"C\"`,
envSyntaxSh,
`A`,
`\"B\" \"C\"`,
`A='\"B\" \"C\"'`,
},
{
`ps A=B C`,
envSyntaxPS,
`A`,
`B C`,
`$Env:A='B C'`,
},
{
`ps A='B C'`,
envSyntaxPS,
`A`,
`'B C'`,
`$Env:A='''B C'''`,
},
{
`ps:export A='B C'`,
envSyntaxPSExport,
`A`,
`'B C'`,
`[Environment]::SetEnvironmentVariable('A', '''B C''', 'User')`,
},
{
`ps:export A=B C`,
envSyntaxPSExport,
`A`,
`B C`,
`[Environment]::SetEnvironmentVariable('A', 'B C', 'User')`,
},
{
`ps:export A="B C"`,
envSyntaxPSExport,
`A`,
`"B C"`,
`[Environment]::SetEnvironmentVariable('A', '"B C"', 'User')`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r, err := tt.e.WriteEnv(tt.key, tt.value)
require.NoError(t, err)
require.Equal(t, tt.want, r)
})
}
}
11 changes: 3 additions & 8 deletions pkg/client/cli/intercept/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,16 +462,11 @@ func (s *state) writeEnvToFileAndClose(file *os.File) (err error) {
sort.Strings(keys)

for _, k := range keys {
if _, err = w.WriteString(k); err != nil {
return err
}
if err = w.WriteByte('='); err != nil {
return err
}
if _, err = w.WriteString(s.env[k]); err != nil {
r, err := s.EnvSyntax.WriteEnv(k, s.env[k])
if err != nil {
return err
}
if err = w.WriteByte('\n'); err != nil {
if _, err = fmt.Fprintln(w, r); err != nil {
return err
}
}
Expand Down
Loading

0 comments on commit d79275f

Please sign in to comment.