Skip to content

Commit

Permalink
fix #12208: gp env to respect workspace env vars
Browse files Browse the repository at this point in the history
  • Loading branch information
akosyakov committed Feb 16, 2023
1 parent 24979dc commit 29972d3
Show file tree
Hide file tree
Showing 13 changed files with 461 additions and 96 deletions.
62 changes: 43 additions & 19 deletions components/gitpod-cli/cmd/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@ import (
"github.com/spf13/cobra"
"golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"

"github.com/gitpod-io/gitpod/common-go/util"
"github.com/gitpod-io/gitpod/gitpod-cli/pkg/supervisor"
serverapi "github.com/gitpod-io/gitpod/gitpod-protocol"
supervisor "github.com/gitpod-io/gitpod/supervisor/api"
supervisorapi "github.com/gitpod-io/gitpod/supervisor/api"
)

var exportEnvs = false
Expand All @@ -31,8 +29,8 @@ var unsetEnvs = false
// envCmd represents the env command
var envCmd = &cobra.Command{
Use: "env",
Short: "Controls user-defined, persistent environment variables.",
Long: `This command can print and modify the persistent environment variables associated with your user, for this repository.
Short: "Controls workspace environment variables.",
Long: `This command can print and modify the persistent environment variables associated with your workspace.
To set the persistent environment variable 'foo' to the value 'bar' use:
gp env foo=bar
Expand Down Expand Up @@ -78,15 +76,20 @@ delete environment variables with a repository pattern of */foo, foo/* or */*.

type connectToServerResult struct {
repositoryPattern string
wsInfo *supervisorapi.WorkspaceInfoResponse
client *serverapi.APIoverJSONRPC

useDeprecatedGetEnvVar bool
}

func connectToServer(ctx context.Context) (*connectToServerResult, error) {
supervisorConn, err := grpc.Dial(util.GetSupervisorAddress(), grpc.WithTransportCredentials(insecure.NewCredentials()))
supervisorClient, err := supervisor.New(ctx)
if err != nil {
return nil, xerrors.Errorf("failed connecting to supervisor: %w", err)
}
wsinfo, err := supervisor.NewInfoServiceClient(supervisorConn).WorkspaceInfo(ctx, &supervisor.WorkspaceInfoRequest{})
defer supervisorClient.Close()

wsinfo, err := supervisorClient.Info.WorkspaceInfo(ctx, &supervisorapi.WorkspaceInfoRequest{})
if err != nil {
return nil, xerrors.Errorf("failed getting workspace info from supervisor: %w", err)
}
Expand All @@ -97,19 +100,35 @@ func connectToServer(ctx context.Context) (*connectToServerResult, error) {
return nil, xerrors.New("repository info is missing owner")
}
if wsinfo.Repository.Name == "" {
return nil, xerrors.New("repository info is missing name")
xerrors.New("repository info is missing name")
}
repositoryPattern := wsinfo.Repository.Owner + "/" + wsinfo.Repository.Name
clientToken, err := supervisor.NewTokenServiceClient(supervisorConn).GetToken(ctx, &supervisor.GetTokenRequest{

var useDeprecatedGetEnvVar bool
clientToken, err := supervisorClient.Token.GetToken(ctx, &supervisorapi.GetTokenRequest{
Host: wsinfo.GitpodApi.Host,
Kind: "gitpod",
Scope: []string{
"function:getEnvVars",
"function:getWorkspaceEnvVars",
"function:setEnvVar",
"function:deleteEnvVar",
"resource:envVar::" + repositoryPattern + "::create/get/update/delete",
},
})
if err != nil {
// TODO remove then GetWorkspaceEnvVars is deployed
clientToken, err = supervisorClient.Token.GetToken(ctx, &supervisorapi.GetTokenRequest{
Host: wsinfo.GitpodApi.Host,
Kind: "gitpod",
Scope: []string{
"function:getEnvVars", // TODO remove then getWorkspaceEnvVars is deployed
"function:setEnvVar",
"function:deleteEnvVar",
"resource:envVar::" + repositoryPattern + "::create/get/update/delete",
},
})
useDeprecatedGetEnvVar = true
}
if err != nil {
return nil, xerrors.Errorf("failed getting token from supervisor: %w", err)
}
Expand All @@ -121,7 +140,7 @@ func connectToServer(ctx context.Context) (*connectToServerResult, error) {
if err != nil {
return nil, xerrors.Errorf("failed connecting to server: %w", err)
}
return &connectToServerResult{repositoryPattern, client}, nil
return &connectToServerResult{repositoryPattern, wsinfo, client, useDeprecatedGetEnvVar}, nil
}

func getEnvs(ctx context.Context) error {
Expand All @@ -131,13 +150,18 @@ func getEnvs(ctx context.Context) error {
}
defer result.client.Close()

vars, err := result.client.GetEnvVars(ctx)
var vars []*serverapi.EnvVar
if !result.useDeprecatedGetEnvVar {
vars, err = result.client.GetWorkspaceEnvVars(ctx, result.wsInfo.WorkspaceId)
} else {
vars, err = result.client.GetEnvVars(ctx)
}
if err != nil {
return xerrors.Errorf("failed to fetch env vars from server: %w", err)
}

for _, v := range vars {
printVar(v, exportEnvs)
printVar(v.Name, v.Value, exportEnvs)
}

return nil
Expand All @@ -163,7 +187,7 @@ func setEnvs(ctx context.Context, args []string) error {
if err != nil {
return err
}
printVar(v, exportEnvs)
printVar(v.Name, v.Value, exportEnvs)
return nil
})
}
Expand All @@ -189,12 +213,12 @@ func deleteEnvs(ctx context.Context, args []string) error {
return g.Wait()
}

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

Expand Down
2 changes: 2 additions & 0 deletions components/gitpod-cli/pkg/supervisor/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ type SupervisorClient struct {
Info api.InfoServiceClient
Notification api.NotificationServiceClient
Control api.ControlServiceClient
Token api.TokenServiceClient
}

type SupervisorClientOption struct {
Expand All @@ -51,6 +52,7 @@ func New(ctx context.Context, options ...*SupervisorClientOption) (*SupervisorCl
Info: api.NewInfoServiceClient(conn),
Notification: api.NewNotificationServiceClient(conn),
Control: api.NewControlServiceClient(conn),
Token: api.NewTokenServiceClient(conn),
}, nil
}

Expand Down
34 changes: 31 additions & 3 deletions components/gitpod-protocol/go/gitpod-service.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ type APIInterface interface {
ClosePort(ctx context.Context, workspaceID string, port float32) (err error)
GetUserStorageResource(ctx context.Context, options *GetUserStorageResourceOptions) (res string, err error)
UpdateUserStorageResource(ctx context.Context, options *UpdateUserStorageResourceOptions) (err error)
GetEnvVars(ctx context.Context) (res []*UserEnvVarValue, err error)
GetWorkspaceEnvVars(ctx context.Context, workspaceID string) (res []*EnvVar, err error)
GetEnvVars(ctx context.Context) (res []*EnvVar, err error)
SetEnvVar(ctx context.Context, variable *UserEnvVarValue) (err error)
DeleteEnvVar(ctx context.Context, variable *UserEnvVarValue) (err error)
HasSSHPublicKey(ctx context.Context) (res bool, err error)
Expand Down Expand Up @@ -1128,15 +1129,35 @@ func (gp *APIoverJSONRPC) UpdateUserStorageResource(ctx context.Context, options
return
}

// GetWorkspaceEnvVars calls GetWorkspaceEnvVars on the server
func (gp *APIoverJSONRPC) GetWorkspaceEnvVars(ctx context.Context, workspaceID string) (res []*EnvVar, err error) {
if gp == nil {
err = errNotConnected
return
}
var _params []interface{}

_params = append(_params, workspaceID)

var result []*EnvVar
err = gp.C.Call(ctx, "GetWorkspaceEnvVars", _params, &result)
if err != nil {
return
}
res = result

return
}

// GetEnvVars calls getEnvVars on the server
func (gp *APIoverJSONRPC) GetEnvVars(ctx context.Context) (res []*UserEnvVarValue, err error) {
func (gp *APIoverJSONRPC) GetEnvVars(ctx context.Context) (res []*EnvVar, err error) {
if gp == nil {
err = errNotConnected
return
}
var _params []interface{}

var result []*UserEnvVarValue
var result []*EnvVar
err = gp.C.Call(ctx, "getEnvVars", _params, &result)
if err != nil {
return
Expand Down Expand Up @@ -1978,6 +1999,13 @@ type WhitelistedRepository struct {
URL string `json:"url,omitempty"`
}

// EnvVar is the EnvVar message type
type EnvVar struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Value string `json:"value,omitempty"`
}

// UserEnvVarValue is the UserEnvVarValue message type
type UserEnvVarValue struct {
ID string `json:"id,omitempty"`
Expand Down
13 changes: 11 additions & 2 deletions components/gitpod-protocol/go/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
UserSSHPublicKeyValue,
SSHPublicKeyValue,
IDESettings,
EnvVarWithValue,
} from "./protocol";
import {
Team,
Expand Down Expand Up @@ -152,6 +153,9 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
getUserStorageResource(options: GitpodServer.GetUserStorageResourceOptions): Promise<string>;
updateUserStorageResource(options: GitpodServer.UpdateUserStorageResourceOptions): Promise<void>;

// Workspace env vars
getWorkspaceEnvVars(workspaceId: string): Promise<EnvVarWithValue[]>;

// User env vars
getEnvVars(): Promise<UserEnvVarValue[]>;
getAllEnvVars(): Promise<UserEnvVarValue[]>;
Expand Down
2 changes: 2 additions & 0 deletions components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,8 @@ export namespace NamedWorkspaceFeatureFlag {
}
}

export type EnvVar = UserEnvVar | ProjectEnvVarWithValue | EnvVarWithValue;

export interface EnvVarWithValue {
name: string;
value: string;
Expand Down
26 changes: 16 additions & 10 deletions components/server/ee/src/prebuilds/prebuild-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
CommitInfo,
PrebuiltWorkspace,
Project,
ProjectEnvVar,
StartPrebuildContext,
StartPrebuildResult,
TaskConfig,
Expand Down Expand Up @@ -39,6 +38,7 @@ import { ResponseError } from "vscode-ws-jsonrpc";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { UserService } from "../../../src/user/user-service";
import { EntitlementService, MayStartWorkspaceResult } from "../../../src/billing/entitlement-service";
import { EnvVarService, ResolvedEnvVars } from "../../../src/workspace/env-var-service";

export class WorkspaceRunningError extends Error {
constructor(msg: string, public instance: WorkspaceInstance) {
Expand Down Expand Up @@ -67,6 +67,7 @@ export class PrebuildManager {
@inject(UserService) protected readonly userService: UserService;
@inject(TeamDB) protected readonly teamDB: TeamDB;
@inject(EntitlementService) protected readonly entitlementService: EntitlementService;
@inject(EnvVarService) private readonly envVarService: EnvVarService;

async abortPrebuildsForBranch(ctx: TraceContext, project: Project, user: User, branch: string): Promise<void> {
const span = TraceContext.startSpan("abortPrebuildsForBranch", ctx);
Expand Down Expand Up @@ -221,8 +222,6 @@ export class PrebuildManager {
}
}

const projectEnvVarsPromise = project ? this.projectService.getProjectEnvironmentVariables(project.id) : [];

let organizationId = (await this.teamDB.findTeamById(project.id))?.id;
if (!user.additionalData?.isMigratedToTeamOnlyAttribution) {
// If the user is not migrated to team-only attribution, we retrieve the organization from the attribution logic.
Expand All @@ -238,6 +237,9 @@ export class PrebuildManager {
prebuildContext,
context.normalizedContextURL!,
);

const envVarsPromise = this.resolveEvnVars(workspace);

const prebuild = await this.workspaceDB.trace({ span }).findPrebuildByWorkspaceID(workspace.id)!;
if (!prebuild) {
throw new Error(`Failed to create a prebuild for: ${context.normalizedContextURL}`);
Expand Down Expand Up @@ -281,8 +283,8 @@ export class PrebuildManager {
await this.workspaceDB.trace({ span }).storePrebuiltWorkspace(prebuild);
} else {
span.setTag("starting", true);
const projectEnvVars = await projectEnvVarsPromise;
await this.workspaceStarter.startWorkspace({ span }, workspace, user, project, [], projectEnvVars, {
const envVars = await envVarsPromise;
await this.workspaceStarter.startWorkspace({ span }, workspace, user, project, envVars, {
excludeFeatureFlags: ["full_workspace_backup"],
});
}
Expand Down Expand Up @@ -341,11 +343,8 @@ export class PrebuildManager {
if (!prebuild) {
throw new Error("No prebuild found for workspace " + workspaceId);
}
let projectEnvVars: ProjectEnvVar[] = [];
if (workspace.projectId) {
projectEnvVars = await this.projectService.getProjectEnvironmentVariables(workspace.projectId);
}
await this.workspaceStarter.startWorkspace({ span }, workspace, user, project, [], projectEnvVars);
const envVars = await this.resolveEvnVars(workspace);
await this.workspaceStarter.startWorkspace({ span }, workspace, user, project, envVars);
return { prebuildId: prebuild.id, wsid: workspace.id, done: false };
} catch (err) {
TraceContext.setError({ span }, err);
Expand Down Expand Up @@ -477,4 +476,11 @@ export class PrebuildManager {
span.finish();
}
}

private resolveEvnVars(workspace: Workspace): Promise<ResolvedEnvVars> {
return this.envVarService.resolve({
user: undefined, // no user specific in prebuild
workspace,
});
}
}
1 change: 1 addition & 0 deletions components/server/src/auth/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const defaultFunctions: FunctionsConfig = {
closePort: { group: "default", points: 1 },
getUserStorageResource: { group: "default", points: 1 },
updateUserStorageResource: { group: "default", points: 1 },
getWorkspaceEnvVars: { group: "default", points: 1 },
getEnvVars: { group: "default", points: 1 },
getAllEnvVars: { group: "default", points: 1 },
setEnvVar: { group: "default", points: 1 },
Expand Down
3 changes: 3 additions & 0 deletions components/server/src/container-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ import { retryMiddleware } from "nice-grpc-client-middleware-retry";
import { IamSessionApp } from "./iam/iam-session-app";
import { spicedbClientFromEnv, SpiceDBClient } from "./authorization/spicedb";
import { Authorizer, PermissionChecker } from "./authorization/perms";
import { EnvVarService } from "./workspace/env-var-service";

export const productionContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
bind(Config).toConstantValue(ConfigFile.fromFile());
Expand Down Expand Up @@ -256,6 +257,8 @@ export const productionContainerModule = new ContainerModule((bind, unbind, isBo

bind(ProjectsService).toSelf().inSingletonScope();

bind(EnvVarService).toSelf().inSingletonScope();

bind(NewsletterSubscriptionController).toSelf().inSingletonScope();

bind<UsageServiceClient>(UsageServiceDefinition.name)
Expand Down
Loading

0 comments on commit 29972d3

Please sign in to comment.