diff --git a/.github/workflows/workflow-approval.yml b/.github/workflows/workflow-approval.yml index ed20463937..e1e442769e 100644 --- a/.github/workflows/workflow-approval.yml +++ b/.github/workflows/workflow-approval.yml @@ -13,10 +13,10 @@ jobs: - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 id: app-token with: - app-id: ${{ secrets.DEVSY_GITHUB_APP_ID }} + client-id: ${{ secrets.DEVSY_GITHUB_APP_ID }} private-key: ${{ secrets.DEVSY_GITHUB_APP_PRIVATE_KEY }} - uses: skevetter/automatic-approve-action@80f16f6e6bea1dfc0f613df222390ee143a4d8e9 # v2 with: token: ${{ steps.app-token.outputs.token }} - workflows: "commit.yml,lint.yml,pr-ci.yml,pre-commit.yml,release-please.yml,promote-release.yml" + workflows: "commit.yml,pr-ci.yml,pre-commit.yml" diff --git a/cmd/workspace/up/configure.go b/cmd/workspace/up/configure.go index 2f7bec4367..6d3182c577 100644 --- a/cmd/workspace/up/configure.go +++ b/cmd/workspace/up/configure.go @@ -77,8 +77,23 @@ func (cmd *UpCmd) openIDE( } ideConfig := client.WorkspaceConfig().IDE - return opener.Open(ctx, ideConfig.Name, ideConfig.Options, opener.IDEParams{ - GPGAgentForwarding: cmd.GPGAgentForwarding, + return opener.Open(ctx, ideConfig.Name, ideConfig.Options, cmd.buildIDEParams( + devsyConfig, + client, + wctx, + )) +} + +func (cmd *UpCmd) buildIDEParams( + devsyConfig *config.Config, + client client2.BaseWorkspaceClient, + wctx *workspaceContext, +) opener.IDEParams { + setupGPGAgentForwarding := cmd.GPGAgentForwarding || + devsyConfig.ContextOptionBool(config.ContextOptionGPGAgentForwarding) + + return opener.IDEParams{ + GPGAgentForwarding: setupGPGAgentForwarding, SSHAuthSockID: cmd.SSHAuthSockID, GitSSHSigningKey: cmd.GitSSHSigningKey, DevsyConfig: devsyConfig, @@ -87,7 +102,7 @@ func (cmd *UpCmd) openIDE( Result: wctx.result, TunnelMode: wctx.tunnelPort > 0, Launch: cmd.IDELaunch, - }) + } } func (cmd *UpCmd) reconfigureSSHWithTunnel( diff --git a/cmd/workspace/up/configure_test.go b/cmd/workspace/up/configure_test.go new file mode 100644 index 0000000000..4b83948b1b --- /dev/null +++ b/cmd/workspace/up/configure_test.go @@ -0,0 +1,86 @@ +package up + +import ( + "testing" + + "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/config" + config2 "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/ide/opener" + provider2 "github.com/devsy-org/devsy/pkg/provider" + "github.com/stretchr/testify/assert" +) + +const vscodeUser = "vscode" + +func TestBuildIDEParams_GPGAgentForwardingParity(t *testing.T) { + tests := []struct { + name string + cmdFlag bool + contextOption string + expected bool + }{ + { + name: "enabled by context option only", + cmdFlag: false, + contextOption: config.BoolTrue, + expected: true, + }, + { + name: "enabled by cli flag only", + cmdFlag: true, + contextOption: config.BoolFalse, + expected: true, + }, + { + name: "disabled when both are off", + cmdFlag: false, + contextOption: config.BoolFalse, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &UpCmd{ + GlobalFlags: &flags.GlobalFlags{}, + CLIOptions: provider2.CLIOptions{ + SSHAuthSockID: "sock-id", + GitSSHSigningKey: "ssh-sign-key", + }, + GPGAgentForwarding: tt.cmdFlag, + IDELaunch: opener.LaunchAuto, + } + + devsyConfig := testConfigWithGPGForwardingOption(tt.contextOption) + wctx := &workspaceContext{ + user: vscodeUser, + result: &config2.Result{}, + tunnelPort: 10800, + } + + params := cmd.buildIDEParams(devsyConfig, nil, wctx) + + assert.Equal(t, tt.expected, params.GPGAgentForwarding) + assert.Equal(t, "sock-id", params.SSHAuthSockID) + assert.Equal(t, "ssh-sign-key", params.GitSSHSigningKey) + assert.Equal(t, vscodeUser, params.User) + assert.Equal(t, wctx.result, params.Result) + assert.True(t, params.TunnelMode) + assert.Equal(t, opener.LaunchAuto, params.Launch) + }) + } +} + +func testConfigWithGPGForwardingOption(value string) *config.Config { + return &config.Config{ + DefaultContext: config.DefaultContext, + Contexts: map[string]*config.ContextConfig{ + config.DefaultContext: { + Options: map[string]config.OptionValue{ + config.ContextOptionGPGAgentForwarding: {Value: value}, + }, + }, + }, + } +} diff --git a/e2e/tests/ide/browser_returns.go b/e2e/tests/ide/browser_returns.go index 8b1f094d11..5c05ef7096 100644 --- a/e2e/tests/ide/browser_returns.go +++ b/e2e/tests/ide/browser_returns.go @@ -16,6 +16,7 @@ import ( "time" "github.com/devsy-org/devsy/e2e/framework" + "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/ide/opener" "github.com/onsi/ginkgo/v2" @@ -365,6 +366,92 @@ var _ = ginkgo.Describe( }, ) + ginkgo.It( + "forwards GPG in browser IDE when enabled only via context option", + ginkgo.Label("gpg"), + ginkgo.SpecTimeout(framework.TimeoutLong()), + func(ctx context.Context) { + f := framework.NewDefaultFramework(initialDir + "/bin") + tempDir, err := framework.CopyToTempDir("tests/ide/testdata-gpg-nonroot") + framework.ExpectNoError(err) + ginkgo.DeferCleanup(framework.CleanupTempDir, initialDir, tempDir) + + contextName := fmt.Sprintf("gpg-ctx-%d", time.Now().UnixNano()) + err = f.DevsyContextCreate(ctx, contextName) + framework.ExpectNoError(err) + ginkgo.DeferCleanup(func(cleanupCtx context.Context) { + _ = f.DevsyContextUse(cleanupCtx, config.DefaultContext) + _ = f.DevsyContextDelete(cleanupCtx, contextName) + }) + + err = f.DevsyContextUse(ctx, contextName) + framework.ExpectNoError(err) + err = f.ExecCommand( + ctx, + false, + true, + "", + []string{ + "context", + "set", + names.Flag(names.Option), + config.ContextOptionGPGAgentForwarding + "=" + config.BoolTrue, + }, + ) + framework.ExpectNoError(err) + + err = f.DevsyProviderAdd(ctx, "docker") + framework.ExpectNoError(err) + err = f.DevsyProviderUse(ctx, "docker") + framework.ExpectNoError(err) + ginkgo.DeferCleanup(func(cleanupCtx context.Context) { + _ = f.DevsyWorkspaceDelete(cleanupCtx, tempDir) + }) + + ginkgo.GinkgoT().Setenv("GNUPGHOME", ginkgo.GinkgoT().TempDir()) + framework.ExpectNoError( + framework.ImportGpgKey( + filepath.Join( + initialDir, + "tests/ssh/testdata/gpg-forwarding/gpg-private.key", + ), + ), + ) + + stdout, stderr, err := f.DevsyUpStreamsRaw(ctx, tempDir, + "--ide=openvscode", "--ide-launch=headless", "--debug") + framework.ExpectNoError(err) + combined := stdout + stderr + gomega.Expect(combined). + To(gomega.ContainSubstring("Starting vscode in browser mode at"), + "expected browser IDE opener path to execute; got:\n%s", combined) + gomega.Expect(combined).To(gomega.ContainSubstring("forwarding gpg-agent"), + "expected browser IDE GPG forward bootstrap to run; got:\n%s", combined) + + gomega.Expect(combined).NotTo(gomega.ContainSubstring("continuing without it"), + "GPG agent forwarding must succeed when enabled from context option only; got:\n%s", combined) + gomega.Expect(combined).NotTo(gomega.ContainSubstring( + "timed out waiting for gpg-agent forward to become ready", + ), "GPG forward should be ready before handing off browser IDE session; got:\n%s", combined) + + ws, err := f.FindWorkspace(ctx, tempDir) + framework.ExpectNoError(err) + state, err := opener.ReadTunnelState(ws.Context, ws.ID) + framework.ExpectNoError(err) + gomega.Expect(state).NotTo(gomega.BeNil(), + "expected browser tunnel state to exist for IDE session") + gomega.Expect(state.PID).To(gomega.BeNumerically(">", 0), + "expected detached browser tunnel process to be running") + + sshCtx, cancelSSH := context.WithDeadline(ctx, time.Now().Add(30*time.Second)) + defer cancelSSH() + err = f.DevsySSHGpgSecretKeyForwarded(sshCtx, tempDir, gpgTestKeyFingerprint) + framework.ExpectNoError(err) + + framework.ExpectNoError(f.DevsyStop(ctx, tempDir)) + }, + ) + ginkgo.It( "does not log 'setup KubeConfig' on a workspace without kubeconfig forwarding", ginkgo.SpecTimeout(framework.TimeoutLong()),