Skip to content

decouple gp env from theia #3569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion components/gitpod-cli/BUILD.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ packages:
- CGO_ENABLED=0
- GOOS=linux
deps:
- components/common-go:lib
- components/supervisor-api/go:lib
- components/gitpod-protocol/go:lib
config:
Expand Down
289 changes: 235 additions & 54 deletions components/gitpod-cli/cmd/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@
package cmd

import (
"context"
"fmt"
"io"
"os"
"strings"
"sync"
"time"

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"golang.org/x/xerrors"
"google.golang.org/grpc"

"github.com/gitpod-io/gitpod/gitpod-cli/pkg/theialib"
serverapi "github.com/gitpod-io/gitpod/gitpod-protocol"
supervisor "github.com/gitpod-io/gitpod/supervisor/api"
)

var exportEnvs = false
Expand Down Expand Up @@ -42,82 +51,254 @@ delete environment variables with a repository pattern of */foo, foo/* or */*.
`,
Args: cobra.ArbitraryArgs,
Run: func(cmd *cobra.Command, args []string) {
fail := func(msg string) {
fmt.Fprintln(os.Stderr, msg)
os.Exit(-1)
log.SetOutput(io.Discard)
f, err := os.OpenFile(os.TempDir()+"/gp-env.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err == nil {
defer f.Close()
log.SetOutput(f)
}

service, err := theialib.NewServiceFromEnv()
if len(args) > 0 {
if unsetEnvs {
deleteEnvs(args)
return
}

setEnvs(args)
} else {
getEnvs()
}
},
}

type connectToServerResult struct {
repositoryPattern string
client *serverapi.APIoverJSONRPC
}

func connectToServer(ctx context.Context) (*connectToServerResult, error) {
supervisorAddr := os.Getenv("SUPERVISOR_ADDR")
if supervisorAddr == "" {
supervisorAddr = "localhost:22999"
}
supervisorConn, err := grpc.Dial(supervisorAddr, grpc.WithInsecure())
if err != nil {
return nil, xerrors.Errorf("failed connecting to supervisor: %w", err)
}
wsinfo, err := supervisor.NewInfoServiceClient(supervisorConn).WorkspaceInfo(ctx, &supervisor.WorkspaceInfoRequest{})
if err != nil {
return nil, xerrors.Errorf("failed getting workspace info from supervisor: %w", err)
}
if wsinfo.Repository == nil {
return nil, xerrors.New("workspace info is missing repository")
}
if wsinfo.Repository.Owner == "" {
return nil, xerrors.New("repository info is missing owner")
}
if wsinfo.Repository.Name == "" {
return nil, xerrors.New("repository info is missing name")
}
repositoryPattern := wsinfo.Repository.Owner + "/" + wsinfo.Repository.Name
clientToken, err := supervisor.NewTokenServiceClient(supervisorConn).GetToken(ctx, &supervisor.GetTokenRequest{
Host: wsinfo.GitpodApi.Host,
Kind: "gitpod",
Scope: []string{
"function:getEnvVars",
"function:setEnvVar",
"function:deleteEnvVar",
"resource:envVar::" + repositoryPattern + "::create/get/update/delete",
},
})
if err != nil {
return nil, xerrors.Errorf("failed getting token from supervisor: %w", err)
}
client, err := serverapi.ConnectToServer(wsinfo.GitpodApi.Endpoint, serverapi.ConnectToServerOpts{
Token: clientToken.Token,
Context: ctx,
Log: log.NewEntry(log.StandardLogger()),
})
if err != nil {
return nil, xerrors.Errorf("failed connecting to server: %w", err)
}
return &connectToServerResult{repositoryPattern, client}, nil
}

func getEnvs() {
if !isTheiaIDE() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
result, err := connectToServer(ctx)
if err != nil {
fail(err.Error())
}

setEnvs := func() {
vars := make([]theialib.EnvironmentVariable, len(args))
for i, arg := range args {
kv := strings.Split(arg, "=")
if len(kv) != 2 {
fail(fmt.Sprintf("%s has no value (correct format is %s=some_value)", arg, arg))
}
vars, err := result.client.GetEnvVars(ctx)
if err != nil {
fail("failed to fetch env vars from server: " + err.Error())
}

key := strings.TrimSpace(kv[0])
if key == "" {
fail(fmt.Sprintf("variable must have a name"))
}
// Do not trim value - the user might want whitespace here
// Also do not check if the value is empty, as an empty value means we want to delete the variable
val := kv[1]
if val == "" {
fail(fmt.Sprintf("variable must have a value; use -u to unset a variable"))
}
for _, v := range vars {
printVar(v, exportEnvs)
}
return
}

vars[i] = theialib.EnvironmentVariable{Name: key, Value: val}
}
service, err := theialib.NewServiceFromEnv()
if err != nil {
fail(err.Error())
}

_, err = service.SetEnvVar(theialib.SetEnvvarRequest{Variables: vars})
if err != nil {
fail(fmt.Sprintf("cannot set environment variables: %v", err))
}
vars, err := service.GetEnvVars(theialib.GetEnvvarsRequest{})
if err != nil {
fail(fmt.Sprintf("cannot get environment variables: %v", err))
}

for _, v := range vars {
printVar(v, exportEnvs)
}
for _, v := range vars.Variables {
printVarFromTheia(v, exportEnvs)
}
}

func setEnvs(args []string) {
if !isTheiaIDE() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
result, err := connectToServer(ctx)
if err != nil {
fail(err.Error())
}
getEnvs := func() {
vars, err := service.GetEnvVars(theialib.GetEnvvarsRequest{})
if err != nil {
fail(fmt.Sprintf("cannot get environment variables: %v", err))

vars := make([]*serverapi.UserEnvVarValue, len(args))
for i, arg := range args {
kv := strings.Split(arg, "=")
if len(kv) != 2 {
fail(fmt.Sprintf("%s has no value (correct format is %s=some_value)", arg, arg))
}

for _, v := range vars.Variables {
printVar(v, exportEnvs)
key := strings.TrimSpace(kv[0])
if key == "" {
fail(fmt.Sprintf("variable must have a name"))
}
}
doUnsetEnvs := func() {
resp, err := service.DeleteEnvVar(theialib.DeleteEnvvarRequest{Variables: args})
if err != nil {
fail(fmt.Sprintf("cannot unset environment variables: %v", err))
// Do not trim value - the user might want whitespace here
// Also do not check if the value is empty, as an empty value means we want to delete the variable
val := kv[1]
if val == "" {
fail(fmt.Sprintf("variable must have a value; use -u to unset a variable"))
}

if len(resp.NotDeleted) != 0 {
fail(fmt.Sprintf("cannot unset environment variables: %s", strings.Join(resp.NotDeleted, ", ")))
}
vars[i] = &serverapi.UserEnvVarValue{Name: key, Value: val, RepositoryPattern: result.repositoryPattern}
}

if len(args) > 0 {
if unsetEnvs {
doUnsetEnvs()
return
}
var exitCode int
var wg sync.WaitGroup
wg.Add(len(vars))
for _, v := range vars {
go func(v *serverapi.UserEnvVarValue) {
err = result.client.SetEnvVar(ctx, v)
if err != nil {
fmt.Fprintln(os.Stderr, fmt.Sprintf("cannot set %s: %v", v.Name, err))
exitCode = -1
} else {
printVar(v, exportEnvs)
}
wg.Done()
}(v)
}
wg.Wait()
os.Exit(exitCode)
}

setEnvs()
} else {
getEnvs()
service, err := theialib.NewServiceFromEnv()
if err != nil {
fail(err.Error())
}

vars := make([]theialib.EnvironmentVariable, len(args))
for i, arg := range args {
kv := strings.Split(arg, "=")
if len(kv) != 2 {
fail(fmt.Sprintf("%s has no value (correct format is %s=some_value)", arg, arg))
}
},

key := strings.TrimSpace(kv[0])
if key == "" {
fail(fmt.Sprintf("variable must have a name"))
}
// Do not trim value - the user might want whitespace here
// Also do not check if the value is empty, as an empty value means we want to delete the variable
val := kv[1]
if val == "" {
fail(fmt.Sprintf("variable must have a value; use -u to unset a variable"))
}

vars[i] = theialib.EnvironmentVariable{Name: key, Value: val}
}

_, err = service.SetEnvVar(theialib.SetEnvvarRequest{Variables: vars})
if err != nil {
fail(fmt.Sprintf("cannot set environment variables: %v", err))
}

for _, v := range vars {
printVarFromTheia(v, exportEnvs)
}
}

func deleteEnvs(args []string) {
if !isTheiaIDE() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
result, err := connectToServer(ctx)
if err != nil {
fail(err.Error())
}

var exitCode int
var wg sync.WaitGroup
wg.Add(len(args))
for _, name := range args {
go func(name string) {
err = result.client.DeleteEnvVar(ctx, &serverapi.UserEnvVarValue{Name: name, RepositoryPattern: result.repositoryPattern})
if err != nil {
fmt.Fprintln(os.Stderr, fmt.Sprintf("cannot unset %s: %v", name, err))
exitCode = -1
}
wg.Done()
}(name)
}
wg.Wait()
os.Exit(exitCode)
}

service, err := theialib.NewServiceFromEnv()
if err != nil {
fail(err.Error())
}

resp, err := service.DeleteEnvVar(theialib.DeleteEnvvarRequest{Variables: args})
if err != nil {
fail(fmt.Sprintf("cannot unset environment variables: %v", err))
}

if len(resp.NotDeleted) != 0 {
fail(fmt.Sprintf("cannot unset environment variables: %s", strings.Join(resp.NotDeleted, ", ")))
}
}

func fail(msg string) {
fmt.Fprintln(os.Stderr, msg)
os.Exit(-1)
}

func printVar(v *serverapi.UserEnvVarValue, export bool) {
val := strings.Replace(v.Value, "\"", "\\\"", -1)
if export {
fmt.Printf("export %s=\"%s\"\n", v.Name, val)
} else {
fmt.Printf("%s=%s\n", v.Name, val)
}
}

func printVar(v theialib.EnvironmentVariable, export bool) {
func printVarFromTheia(v theialib.EnvironmentVariable, export bool) {
val := strings.Replace(v.Value, "\"", "\\\"", -1)
if export {
fmt.Printf("export %s=\"%s\"\n", v.Name, val)
Expand Down
14 changes: 13 additions & 1 deletion components/gitpod-cli/cmd/git-token-validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package cmd
import (
"context"
"fmt"
"io"
"os"
"strings"
"time"
Expand Down Expand Up @@ -35,6 +36,13 @@ var gitTokenValidator = &cobra.Command{
Args: cobra.ExactArgs(0),
Hidden: true,
Run: func(cmd *cobra.Command, args []string) {
log.SetOutput(io.Discard)
Copy link
Member Author

@akosyakov akosyakov Mar 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JanKoehnlein Does it make sense to you? Otherwise I am not sure where everything get logged?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it makes sense. I am just not sure whether we should use another file. The credential helper could still have the same file open for appending, as it starts the token validator in background and then releases the file on exit. Anyway, this shouldn't be a problem on Linux.

f, err := os.OpenFile(os.TempDir()+"/gitpod-git-credential-helper.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err == nil {
defer f.Close()
log.SetOutput(f)
}

log.Infof("gp git-token-validator")

ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
Expand All @@ -61,7 +69,11 @@ var gitTokenValidator = &cobra.Command{
if err != nil {
log.WithError(err).Fatal("error getting token from supervisor")
}
client, err := serverapi.ConnectToServer(wsinfo.GitpodApi.Endpoint, serverapi.ConnectToServerOpts{Token: clientToken.Token, Context: ctx})
client, err := serverapi.ConnectToServer(wsinfo.GitpodApi.Endpoint, serverapi.ConnectToServerOpts{
Token: clientToken.Token,
Context: ctx,
Log: log.NewEntry(log.StandardLogger()),
})
if err != nil {
log.WithError(err).Fatal("error connecting to server")
}
Expand Down
Loading