Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/gitpod-cli/cmd/env.go
2498 views
1
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package cmd
6
7
import (
8
"context"
9
"fmt"
10
"io"
11
"net/http"
12
"os"
13
"strings"
14
"sync"
15
"time"
16
17
log "github.com/sirupsen/logrus"
18
"github.com/sourcegraph/jsonrpc2"
19
"github.com/spf13/cobra"
20
"golang.org/x/sync/errgroup"
21
"golang.org/x/xerrors"
22
23
"github.com/gitpod-io/gitpod/gitpod-cli/pkg/supervisor"
24
"github.com/gitpod-io/gitpod/gitpod-cli/pkg/utils"
25
serverapi "github.com/gitpod-io/gitpod/gitpod-protocol"
26
"github.com/gitpod-io/gitpod/supervisor/api"
27
supervisorapi "github.com/gitpod-io/gitpod/supervisor/api"
28
)
29
30
var exportEnvs = false
31
var unsetEnvs = false
32
var scope = string(envScopeRepo)
33
34
type envScope string
35
36
var (
37
envScopeRepo envScope = "repo"
38
envScopeUser envScope = "user"
39
envScopeLegacyUser envScope = "legacy-user"
40
)
41
42
func envScopeFromString(s string) envScope {
43
switch s {
44
case string(envScopeRepo):
45
return envScopeRepo
46
case string(envScopeUser):
47
return envScopeUser
48
default:
49
return envScopeRepo
50
}
51
}
52
53
// envCmd represents the env command
54
var envCmd = &cobra.Command{
55
Use: "env",
56
Short: "Controls workspace environment variables.",
57
Long: `This command can print and modify the persistent environment variables associated with your workspace.
58
59
To set the persistent environment variable 'foo' to the value 'bar' use:
60
gp env foo=bar
61
62
Beware that this does not modify your current terminal session, but rather persists this variable for the next workspace on this repository.
63
This command can only interact with environment variables for this repository. If you want to set that environment variable in your terminal,
64
you can do so using -e:
65
eval $(gp env -e foo=bar)
66
67
To update the current terminal session with the latest set of persistent environment variables, use:
68
eval $(gp env -e)
69
70
To delete a persistent environment variable use:
71
gp env -u foo
72
73
Note that you can delete/unset variables if their repository pattern matches the repository of this workspace exactly. I.e. you cannot
74
delete environment variables with a repository pattern of */foo, foo/* or */*.
75
`,
76
Args: cobra.ArbitraryArgs,
77
RunE: func(cmd *cobra.Command, args []string) error {
78
log.SetOutput(io.Discard)
79
f, err := os.OpenFile(os.TempDir()+"/gp-env.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
80
if err == nil {
81
defer f.Close()
82
log.SetOutput(f)
83
}
84
85
ctx, cancel := context.WithTimeout(cmd.Context(), 1*time.Minute)
86
defer cancel()
87
88
if len(args) > 0 {
89
if unsetEnvs {
90
err = deleteEnvs(ctx, args)
91
} else {
92
setEnvScope := envScopeFromString(scope)
93
err = setEnvs(ctx, setEnvScope, args)
94
}
95
} else {
96
err = getEnvs(ctx)
97
}
98
return err
99
},
100
}
101
102
type connectToServerResult struct {
103
repositoryPattern string
104
wsInfo *supervisorapi.WorkspaceInfoResponse
105
client *serverapi.APIoverJSONRPC
106
gitpodHost string
107
}
108
109
type connectToServerOptions struct {
110
supervisorClient *supervisor.SupervisorClient
111
wsInfo *api.WorkspaceInfoResponse
112
log *log.Entry
113
114
setEnvScope envScope
115
}
116
117
func connectToServer(ctx context.Context, options *connectToServerOptions) (*connectToServerResult, error) {
118
var err error
119
var supervisorClient *supervisor.SupervisorClient
120
if options != nil && options.supervisorClient != nil {
121
supervisorClient = options.supervisorClient
122
} else {
123
supervisorClient, err = supervisor.New(ctx)
124
if err != nil {
125
return nil, xerrors.Errorf("failed connecting to supervisor: %w", err)
126
}
127
defer supervisorClient.Close()
128
}
129
130
var wsinfo *api.WorkspaceInfoResponse
131
if options != nil && options.wsInfo != nil {
132
wsinfo = options.wsInfo
133
} else {
134
wsinfo, err = supervisorClient.Info.WorkspaceInfo(ctx, &supervisorapi.WorkspaceInfoRequest{})
135
if err != nil {
136
return nil, xerrors.Errorf("failed getting workspace info from supervisor: %w", err)
137
}
138
}
139
if wsinfo.Repository == nil {
140
return nil, xerrors.New("workspace info is missing repository")
141
}
142
if wsinfo.Repository.Owner == "" {
143
return nil, xerrors.New("repository info is missing owner")
144
}
145
if wsinfo.Repository.Name == "" {
146
return nil, xerrors.New("repository info is missing name")
147
}
148
repositoryPattern := wsinfo.Repository.Owner + "/" + wsinfo.Repository.Name
149
150
operations := "create/get/update/delete"
151
if options != nil && options.setEnvScope == envScopeUser {
152
// Updating user env vars requires a different token with a special scope
153
repositoryPattern = "*/**"
154
operations = "update"
155
}
156
if options != nil && options.setEnvScope == envScopeLegacyUser {
157
// Updating user env vars requires a different token with a special scope
158
repositoryPattern = "*/*"
159
operations = "update"
160
}
161
162
clientToken, err := supervisorClient.Token.GetToken(ctx, &supervisorapi.GetTokenRequest{
163
Host: wsinfo.GitpodApi.Host,
164
Kind: "gitpod",
165
Scope: []string{
166
"function:getWorkspaceEnvVars",
167
"function:setEnvVar",
168
"function:deleteEnvVar",
169
"resource:envVar::" + repositoryPattern + "::" + operations,
170
},
171
})
172
if err != nil {
173
return nil, xerrors.Errorf("failed getting token from supervisor: %w", err)
174
}
175
var serverLog *log.Entry
176
if options != nil && options.log != nil {
177
serverLog = options.log
178
} else {
179
serverLog = log.NewEntry(log.StandardLogger())
180
}
181
client, err := serverapi.ConnectToServer(wsinfo.GitpodApi.Endpoint, serverapi.ConnectToServerOpts{
182
Token: clientToken.Token,
183
Context: ctx,
184
Log: serverLog,
185
})
186
if err != nil {
187
return nil, xerrors.Errorf("failed connecting to server: %w", err)
188
}
189
return &connectToServerResult{repositoryPattern, wsinfo, client, wsinfo.GitpodHost}, nil
190
}
191
192
func getWorkspaceEnvs(ctx context.Context, options *connectToServerOptions) ([]*serverapi.EnvVar, error) {
193
result, err := connectToServer(ctx, options)
194
if err != nil {
195
return nil, err
196
}
197
defer result.client.Close()
198
199
return result.client.GetWorkspaceEnvVars(ctx, result.wsInfo.WorkspaceId)
200
}
201
202
func getEnvs(ctx context.Context) error {
203
vars, err := getWorkspaceEnvs(ctx, nil)
204
if err != nil {
205
return xerrors.Errorf("failed to fetch env vars from server: %w", err)
206
}
207
208
for _, v := range vars {
209
printVar(v.Name, v.Value, exportEnvs)
210
}
211
212
return nil
213
}
214
215
func setEnvs(ctx context.Context, setEnvScope envScope, args []string) error {
216
options := connectToServerOptions{
217
setEnvScope: setEnvScope,
218
}
219
result, err := connectToServer(ctx, &options)
220
if err != nil {
221
return err
222
}
223
defer result.client.Close()
224
225
vars, err := parseArgs(args, result.repositoryPattern)
226
if err != nil {
227
return err
228
}
229
230
g, ctx := errgroup.WithContext(ctx)
231
for _, v := range vars {
232
v := v
233
g.Go(func() error {
234
err = result.client.SetEnvVar(ctx, v)
235
if err != nil {
236
if ferr, ok := err.(*jsonrpc2.Error); ok && ferr.Code == http.StatusForbidden && setEnvScope == envScopeUser {
237
// If we tried updating an env var with */** and it doesn't exist, it may exist with the */* scope
238
options.setEnvScope = envScopeLegacyUser
239
result, err := connectToServer(ctx, &options)
240
if err != nil {
241
return err
242
}
243
defer result.client.Close()
244
245
v.RepositoryPattern = "*/*"
246
err = result.client.SetEnvVar(ctx, v)
247
if ferr, ok := err.(*jsonrpc2.Error); ok && ferr.Code == http.StatusForbidden {
248
fmt.Println(ferr.Message, ferr.Data)
249
return fmt.Errorf(""+
250
"Can't automatically create env var `%s` for security reasons.\n"+
251
"Please create the var manually under %s/user/variables using Name=%s, Scope=*/**, Value=foobar", v.Name, result.gitpodHost, v.Name)
252
}
253
} else {
254
return err
255
}
256
}
257
printVar(v.Name, v.Value, exportEnvs)
258
return nil
259
})
260
}
261
return g.Wait()
262
}
263
264
func deleteEnvs(ctx context.Context, args []string) error {
265
result, err := connectToServer(ctx, nil)
266
if err != nil {
267
return err
268
}
269
defer result.client.Close()
270
271
g, ctx := errgroup.WithContext(ctx)
272
var wg sync.WaitGroup
273
wg.Add(len(args))
274
for _, name := range args {
275
name := name
276
g.Go(func() error {
277
return result.client.DeleteEnvVar(ctx, &serverapi.UserEnvVarValue{Name: name, RepositoryPattern: result.repositoryPattern})
278
})
279
}
280
return g.Wait()
281
}
282
283
func printVar(name string, value string, export bool) {
284
val := strings.Replace(value, "\"", "\\\"", -1)
285
if export {
286
fmt.Printf("export %s=\"%s\"\n", name, val)
287
} else {
288
fmt.Printf("%s=%s\n", name, val)
289
}
290
}
291
292
func parseArgs(args []string, pattern string) ([]*serverapi.UserEnvVarValue, error) {
293
vars := make([]*serverapi.UserEnvVarValue, len(args))
294
for i, arg := range args {
295
if arg == "" {
296
return nil, GpError{Err: xerrors.Errorf("empty string (correct format is key=value)"), OutCome: utils.Outcome_UserErr, ErrorCode: utils.UserErrorCode_InvalidArguments}
297
}
298
299
if !strings.Contains(arg, "=") {
300
return nil, GpError{Err: xerrors.Errorf("%s has no equal character (correct format is %s=some_value)", arg, arg), OutCome: utils.Outcome_UserErr, ErrorCode: utils.UserErrorCode_InvalidArguments}
301
}
302
303
parts := strings.SplitN(arg, "=", 2)
304
if len(parts) != 2 {
305
return nil, GpError{Err: xerrors.Errorf("invalid format: %s (correct format is key=value)", arg), OutCome: utils.Outcome_UserErr, ErrorCode: utils.UserErrorCode_InvalidArguments}
306
}
307
308
key := strings.TrimSpace(parts[0])
309
if key == "" {
310
return nil, GpError{Err: xerrors.Errorf("variable must have a name"), OutCome: utils.Outcome_UserErr, ErrorCode: utils.UserErrorCode_InvalidArguments}
311
}
312
313
// Do not trim value - the user might want whitespace here
314
// Also do not check if the value is empty, as an empty value means we want to delete the variable
315
val := parts[1]
316
// the value could be defined with known separators
317
val = strings.Trim(val, `"`)
318
val = strings.Trim(val, `'`)
319
val = strings.ReplaceAll(val, `\ `, " ")
320
321
if val == "" {
322
return nil, GpError{Err: xerrors.Errorf("variable must have a value; use -u to unset a variable"), OutCome: utils.Outcome_UserErr, ErrorCode: utils.UserErrorCode_InvalidArguments}
323
}
324
325
vars[i] = &serverapi.UserEnvVarValue{Name: key, Value: val, RepositoryPattern: pattern}
326
}
327
328
return vars, nil
329
}
330
331
func init() {
332
rootCmd.AddCommand(envCmd)
333
334
envCmd.Flags().BoolVarP(&exportEnvs, "export", "e", false, "produce a script that can be eval'ed in Bash")
335
envCmd.Flags().BoolVarP(&unsetEnvs, "unset", "u", false, "deletes/unsets persisted environment variables")
336
envCmd.Flags().StringVarP(&scope, "scope", "s", "repo", "deletes/unsets persisted environment variables")
337
}
338
339