diff --git a/cmd/daytona/config/const.go b/cmd/daytona/config/const.go index ce64d5cfde..4a75d6bbea 100644 --- a/cmd/daytona/config/const.go +++ b/cmd/daytona/config/const.go @@ -89,6 +89,23 @@ func GetDocsLinkFromGitProvider(providerId string) string { } } +func GetDocsLinkForCommitSigning(providerId string) string { + switch providerId { + case "github", "github-enterprise-server": + return "https://docs.github.com/en/authentication/managing-commit-signature-verification" + case "gitlab", "gitlab-self-managed": + return "https://docs.gitlab.com/ee/user/project/repository/signed_commits" + case "gitea": + return "https://docs.gitea.com/administration/signing" + case "azure-devops": + return "https://learn.microsoft.com/en-us/azure/devops/repos/git/use-ssh-keys-to-authenticate?view=azure-devops" + case "aws-codecommit": + return "https://docs.aws.amazon.com/codecommit/latest/userguide/setting-up-ssh-unixes.html" + default: + return "" + } +} + func GetRequiredScopesFromGitProviderId(providerId string) string { switch providerId { case "github": diff --git a/cmd/daytona/config/ssh_file.go b/cmd/daytona/config/ssh_file.go index 79aab24798..36408bb79b 100644 --- a/cmd/daytona/config/ssh_file.go +++ b/cmd/daytona/config/ssh_file.go @@ -4,12 +4,16 @@ package config import ( + "bytes" "fmt" "os" + "os/exec" "path/filepath" "regexp" "runtime" "strings" + + log "github.com/sirupsen/logrus" ) var sshHomeDir string @@ -104,7 +108,7 @@ func UnlinkSshFiles() error { // Add ssh entry -func generateSshConfigEntry(profileId, workspaceId, projectName, knownHostsPath string) (string, error) { +func generateSshConfigEntry(profileId, workspaceId, projectName, knownHostsPath string, gpgForward bool) (string, error) { daytonaPath, err := os.Executable() if err != nil { return "", err @@ -118,23 +122,33 @@ func generateSshConfigEntry(profileId, workspaceId, projectName, knownHostsPath tab+"StrictHostKeyChecking no\n"+ tab+"UserKnownHostsFile %s\n"+ tab+"ProxyCommand \"%s\" ssh-proxy %s %s %s\n"+ - tab+"ForwardAgent yes\n\n", projectHostname, knownHostsPath, daytonaPath, profileId, workspaceId, projectName) + tab+"ForwardAgent yes\n", projectHostname, knownHostsPath, daytonaPath, profileId, workspaceId, projectName) - return config, nil -} + if gpgForward { + localSocket, err := getLocalGPGSocket() + if err != nil { + log.Trace(err) + return config, nil + } -func EnsureSshConfigEntryAdded(profileId, workspaceName, projectName string) error { - err := ensureSshFilesLinked() - if err != nil { - return err - } + remoteSocket, err := getRemoteGPGSocket(projectHostname) + if err != nil { + log.Trace(err) + return config, nil + } - knownHostsFile := "/dev/null" - if runtime.GOOS == "windows" { - knownHostsFile = "NUL" + config += fmt.Sprintf( + tab+"StreamLocalBindUnlink yes\n"+ + tab+"RemoteForward %s:%s\n\n", remoteSocket, localSocket) + } else { + config += "\n" } - data, err := generateSshConfigEntry(profileId, workspaceName, projectName, knownHostsFile) + return config, nil +} + +func EnsureSshConfigEntryAdded(profileId, workspaceName, projectName string, gpgKey string) error { + err := ensureSshFilesLinked() if err != nil { return err } @@ -142,50 +156,123 @@ func EnsureSshConfigEntryAdded(profileId, workspaceName, projectName string) err sshDir := filepath.Join(sshHomeDir, ".ssh") configPath := filepath.Join(sshDir, "daytona_config") + knownHostsFile := getKnownHostsFile() + // Read existing content from the file existingContent, err := os.ReadFile(configPath) if err != nil && !os.IsNotExist(err) { return err } - if strings.Contains(string(existingContent), data) { - return nil + // Generate SSH config entry without GPG forwarding + newContent, err := appendSshConfigEntry(configPath, profileId, workspaceName, projectName, knownHostsFile, false, string(existingContent)) + if err != nil { + return err } - // Combine the new data with existing content - newData := data + string(existingContent) + if gpgKey != "" { + // Generate SSH config entry with GPG forwarding and override previous config + _, err := appendSshConfigEntry(configPath, profileId, workspaceName, projectName, knownHostsFile, true, newContent) + if err != nil { + return err + } + projectHostname := GetProjectHostname(profileId, workspaceName, projectName) + err = ExportGPGKey(gpgKey, projectHostname) + if err != nil { + return err + } + } + + return nil +} + +func getKnownHostsFile() string { + if runtime.GOOS == "windows" { + return "NUL" + } + return "/dev/null" +} + +func appendSshConfigEntry(configPath, profileId, workspaceId, projectName, knownHostsFile string, gpgForward bool, existingContent string) (string, error) { + data, err := generateSshConfigEntry(profileId, workspaceId, projectName, knownHostsFile, gpgForward) + if err != nil { + return "", err + } + + if strings.Contains(existingContent, data) { + // Entry already exists in the file + return existingContent, nil + } + + // We want to remove the config entry gpg counterpart + configCounterpart, err := generateSshConfigEntry(profileId, workspaceId, projectName, knownHostsFile, !gpgForward) + if err != nil { + return "", err + } + updatedContent := strings.ReplaceAll(existingContent, configCounterpart, "") + updatedContent = data + updatedContent // Open the file for writing file, err := os.Create(configPath) if err != nil { - return err + return "", err } defer file.Close() - _, err = file.WriteString(newData) + _, err = file.WriteString(updatedContent) + return updatedContent, err +} + +func getLocalGPGSocket() (string, error) { + // Check if gpg is installed + if _, err := exec.LookPath("gpg"); err != nil { + return "", fmt.Errorf("gpg is not installed: %v", err) + } + + // Attempt to get the local GPG socket + cmd := exec.Command("gpgconf", "--list-dir", "agent-extra-socket") + output, err := cmd.Output() if err != nil { - return err + return "", fmt.Errorf("failed to get local GPG socket: %v", err) } + return strings.TrimSpace(string(output)), nil +} - return nil +func getRemoteGPGSocket(projectHostname string) (string, error) { + cmd := exec.Command("ssh", projectHostname, "gpgconf --list-dir agent-socket") + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("failed to get remote GPG socket: %v", err) + } + return strings.TrimSpace(string(output)), nil } -func RemoveWorkspaceSshEntries(profileId, workspaceId string) error { - sshDir := filepath.Join(sshHomeDir, ".ssh") - configPath := filepath.Join(sshDir, "daytona_config") +func ExportGPGKey(keyID, projectHostname string) error { + exportCmd := exec.Command("gpg", "--export", keyID) + var output bytes.Buffer + exportCmd.Stdout = &output - // Read existing content from the file - existingContent, err := os.ReadFile(configPath) - if err != nil && !os.IsNotExist(err) { - return nil + if err := exportCmd.Run(); err != nil { + return err } - regex := regexp.MustCompile(fmt.Sprintf(`Host %s-%s-\w+\n(?:\t.*\n?)*`, profileId, workspaceId)) - newContent := regex.ReplaceAllString(string(existingContent), "") + importCmd := exec.Command("ssh", projectHostname, "gpg --import") + importCmd.Stdin = &output + + return importCmd.Run() +} + +func readSshConfig(configPath string) (string, error) { + content, err := os.ReadFile(configPath) + if err != nil && !os.IsNotExist(err) { + return "", err + } + return string(content), nil +} +func writeSshConfig(configPath, newContent string) error { newContent = strings.Trim(newContent, "\n") - // Open the file for writing file, err := os.Create(configPath) if err != nil { return err @@ -196,6 +283,31 @@ func RemoveWorkspaceSshEntries(profileId, workspaceId string) error { if err != nil { return err } + return nil +} + +// RemoveWorkspaceSshEntries removes all SSH entries for a given profileId and workspaceId +func RemoveWorkspaceSshEntries(profileId, workspaceId string) error { + sshDir := filepath.Join(os.Getenv("HOME"), ".ssh") + configPath := filepath.Join(sshDir, "daytona_config") + + // Read existing content from the SSH config file + existingContent, err := readSshConfig(configPath) + if err != nil { + return err + } + + // Define the regex pattern to match Host entries for the given profileId and workspaceId + regex := regexp.MustCompile(fmt.Sprintf(`Host %s-%s-\w+\s*\n(?:\t.*\n?)*`, profileId, workspaceId)) + + // Replace matched entries with an empty string + newContent := regex.ReplaceAllString(existingContent, "") + + // Write the updated content back to the config file + err = writeSshConfig(configPath, newContent) + if err != nil { + return err + } return nil } diff --git a/internal/testing/git/mocks/gitservice.go b/internal/testing/git/mocks/gitservice.go index fb70ef17dd..99ea2a30a8 100644 --- a/internal/testing/git/mocks/gitservice.go +++ b/internal/testing/git/mocks/gitservice.go @@ -31,8 +31,8 @@ func (m *MockGitService) RepositoryExists() (bool, error) { return args.Bool(0), args.Error(1) } -func (m *MockGitService) SetGitConfig(userData *gitprovider.GitUser) error { - args := m.Called(userData) +func (m *MockGitService) SetGitConfig(userData *gitprovider.GitUser, providerConfig *gitprovider.GitProviderConfig) error { + args := m.Called(userData, providerConfig) return args.Error(0) } diff --git a/internal/util/path.go b/internal/util/path.go index 4647b06295..482cf6075e 100644 --- a/internal/util/path.go +++ b/internal/util/path.go @@ -11,8 +11,8 @@ import ( "github.com/daytonaio/daytona/cmd/daytona/config" ) -func GetHomeDir(activeProfile config.Profile, workspaceId string, projectName string) (string, error) { - err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName) +func GetHomeDir(activeProfile config.Profile, workspaceId string, projectName string, gpgKey string) (string, error) { + err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName, gpgKey) if err != nil { return "", err } @@ -27,8 +27,8 @@ func GetHomeDir(activeProfile config.Profile, workspaceId string, projectName st return strings.TrimRight(string(homeDir), "\n"), nil } -func GetProjectDir(activeProfile config.Profile, workspaceId string, projectName string) (string, error) { - err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName) +func GetProjectDir(activeProfile config.Profile, workspaceId string, projectName string, gpgKey string) (string, error) { + err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName, gpgKey) if err != nil { return "", err } @@ -44,7 +44,7 @@ func GetProjectDir(activeProfile config.Profile, workspaceId string, projectName return strings.TrimRight(string(daytonaProjectDir), "\n"), nil } - homeDir, err := GetHomeDir(activeProfile, workspaceId, projectName) + homeDir, err := GetHomeDir(activeProfile, workspaceId, projectName, gpgKey) if err != nil { return "", err } diff --git a/pkg/agent/agent.go b/pkg/agent/agent.go index 80f28986d8..b859089454 100644 --- a/pkg/agent/agent.go +++ b/pkg/agent/agent.go @@ -112,7 +112,14 @@ func (a *Agent) startProjectMode() error { } } - err = a.Git.SetGitConfig(gitUser) + var providerConfig *gitprovider.GitProviderConfig + if gitProvider != nil { + providerConfig = &gitprovider.GitProviderConfig{ + SigningMethod: (*gitprovider.SigningMethod)(gitProvider.SigningMethod), + SigningKey: gitProvider.SigningKey, + } + } + err = a.Git.SetGitConfig(gitUser, providerConfig) if err != nil { log.Error(fmt.Sprintf("failed to set git config: %s", err)) } diff --git a/pkg/agent/agent_test.go b/pkg/agent/agent_test.go index 8fcfaa2c89..d5fdfb760d 100644 --- a/pkg/agent/agent_test.go +++ b/pkg/agent/agent_test.go @@ -75,7 +75,7 @@ func TestAgent(t *testing.T) { mockGitService := mock_git.NewMockGitService() mockGitService.On("RepositoryExists").Return(true, nil) - mockGitService.On("SetGitConfig", mock.Anything).Return(nil) + mockGitService.On("SetGitConfig", mock.Anything, mock.Anything).Return(nil) mockGitService.On("GetGitStatus").Return(gitStatus1, nil) mockSshServer := mocks.NewMockSshServer() diff --git a/pkg/api/controllers/gitprovider/dto/dto.go b/pkg/api/controllers/gitprovider/dto/dto.go index 1e93e4e0d3..9a46b18219 100644 --- a/pkg/api/controllers/gitprovider/dto/dto.go +++ b/pkg/api/controllers/gitprovider/dto/dto.go @@ -3,15 +3,21 @@ package dto +import ( + "github.com/daytonaio/daytona/pkg/gitprovider" +) + type RepositoryUrl struct { URL string `json:"url" validate:"required"` } // @name RepositoryUrl type SetGitProviderConfig struct { - Id string `json:"id" validate:"optional"` - ProviderId string `json:"providerId" validate:"required"` - Username *string `json:"username,omitempty" validate:"optional"` - Token string `json:"token" validate:"required"` - BaseApiUrl *string `json:"baseApiUrl,omitempty" validate:"optional"` - Alias *string `json:"alias,omitempty" validate:"optional"` + Id string `json:"id" validate:"optional"` + ProviderId string `json:"providerId" validate:"required"` + Username *string `json:"username,omitempty" validate:"optional"` + Token string `json:"token" validate:"required"` + BaseApiUrl *string `json:"baseApiUrl,omitempty" validate:"optional"` + Alias *string `json:"alias,omitempty" validate:"optional"` + SigningKey *string `json:"signingKey,omitempty" validate:"optional"` + SigningMethod *gitprovider.SigningMethod `json:"signingMethod,omitempty" validate:"optional"` } // @name SetGitProviderConfig diff --git a/pkg/api/controllers/gitprovider/gitprovider.go b/pkg/api/controllers/gitprovider/gitprovider.go index 315f12dca4..004422b20f 100644 --- a/pkg/api/controllers/gitprovider/gitprovider.go +++ b/pkg/api/controllers/gitprovider/gitprovider.go @@ -42,6 +42,7 @@ func ListGitProviders(ctx *gin.Context) { for _, provider := range response { provider.Token = "" + provider.SigningKey = nil } ctx.JSON(200, response) @@ -111,6 +112,11 @@ func GetGitProvider(ctx *gin.Context) { return } + apiKeyType, ok := ctx.Get("apiKeyType") + if !ok || apiKeyType == apikey.ApiKeyTypeClient { + gitProvider.Token = "" + } + ctx.JSON(200, gitProvider) } @@ -170,10 +176,12 @@ func SetGitProvider(ctx *gin.Context) { } gitProviderConfig := gitprovider.GitProviderConfig{ - Id: setConfigDto.Id, - ProviderId: setConfigDto.ProviderId, - Token: setConfigDto.Token, - BaseApiUrl: setConfigDto.BaseApiUrl, + Id: setConfigDto.Id, + ProviderId: setConfigDto.ProviderId, + Token: setConfigDto.Token, + BaseApiUrl: setConfigDto.BaseApiUrl, + SigningKey: setConfigDto.SigningKey, + SigningMethod: setConfigDto.SigningMethod, } if setConfigDto.Username != nil { diff --git a/pkg/api/docs/docs.go b/pkg/api/docs/docs.go index 73be40e1a5..1a6420214a 100644 --- a/pkg/api/docs/docs.go +++ b/pkg/api/docs/docs.go @@ -2273,6 +2273,12 @@ const docTemplate = `{ "providerId": { "type": "string" }, + "signingKey": { + "type": "string" + }, + "signingMethod": { + "$ref": "#/definitions/SigningMethod" + }, "token": { "type": "string" }, @@ -2818,6 +2824,12 @@ const docTemplate = `{ "providerId": { "type": "string" }, + "signingKey": { + "type": "string" + }, + "signingMethod": { + "$ref": "#/definitions/SigningMethod" + }, "token": { "type": "string" }, @@ -2840,6 +2852,17 @@ const docTemplate = `{ } } }, + "SigningMethod": { + "type": "string", + "enum": [ + "ssh", + "gpg" + ], + "x-enum-varnames": [ + "SigningMethodSSH", + "SigningMethodGPG" + ] + }, "Status": { "type": "string", "enum": [ diff --git a/pkg/api/docs/swagger.json b/pkg/api/docs/swagger.json index 074231de34..16b10e422d 100644 --- a/pkg/api/docs/swagger.json +++ b/pkg/api/docs/swagger.json @@ -2270,6 +2270,12 @@ "providerId": { "type": "string" }, + "signingKey": { + "type": "string" + }, + "signingMethod": { + "$ref": "#/definitions/SigningMethod" + }, "token": { "type": "string" }, @@ -2815,6 +2821,12 @@ "providerId": { "type": "string" }, + "signingKey": { + "type": "string" + }, + "signingMethod": { + "$ref": "#/definitions/SigningMethod" + }, "token": { "type": "string" }, @@ -2837,6 +2849,17 @@ } } }, + "SigningMethod": { + "type": "string", + "enum": [ + "ssh", + "gpg" + ], + "x-enum-varnames": [ + "SigningMethodSSH", + "SigningMethodGPG" + ] + }, "Status": { "type": "string", "enum": [ diff --git a/pkg/api/docs/swagger.yaml b/pkg/api/docs/swagger.yaml index 6762a3125f..9c15a45ad6 100644 --- a/pkg/api/docs/swagger.yaml +++ b/pkg/api/docs/swagger.yaml @@ -292,6 +292,10 @@ definitions: type: string providerId: type: string + signingKey: + type: string + signingMethod: + $ref: '#/definitions/SigningMethod' token: type: string username: @@ -672,6 +676,10 @@ definitions: type: string providerId: type: string + signingKey: + type: string + signingMethod: + $ref: '#/definitions/SigningMethod' token: type: string username: @@ -689,6 +697,14 @@ definitions: required: - uptime type: object + SigningMethod: + enum: + - ssh + - gpg + type: string + x-enum-varnames: + - SigningMethodSSH + - SigningMethodGPG Status: enum: - Unmodified diff --git a/pkg/api/server.go b/pkg/api/server.go index c341fe7607..cb66be76d2 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -233,6 +233,7 @@ func (a *ApiServer) Start() error { gitProviderController.POST("/context/url", gitprovider.GetUrlFromRepository) gitProviderController.GET("/for-url/:url", gitprovider.ListGitProvidersForUrl) gitProviderController.GET("/id-for-url/:url", gitprovider.GetGitProviderIdForUrl) + gitProviderController.GET("/:gitProviderId", gitprovider.GetGitProvider) } apiKeyController := protected.Group("/apikey") @@ -258,7 +259,6 @@ func (a *ApiServer) Start() error { projectGroup.Use(middlewares.ProjectAuthMiddleware()) { projectGroup.POST(workspaceController.BasePath()+"/:workspaceId/:projectId/state", workspace.SetProjectState) - projectGroup.GET(gitProviderController.BasePath()+"/:gitProviderId", gitprovider.GetGitProvider) } a.httpServer = &http.Server{ diff --git a/pkg/apiclient/README.md b/pkg/apiclient/README.md index a9f9f4a69e..5c2e1633f7 100644 --- a/pkg/apiclient/README.md +++ b/pkg/apiclient/README.md @@ -188,6 +188,7 @@ Class | Method | HTTP request | Description - [ServerConfig](docs/ServerConfig.md) - [SetGitProviderConfig](docs/SetGitProviderConfig.md) - [SetProjectState](docs/SetProjectState.md) + - [SigningMethod](docs/SigningMethod.md) - [Status](docs/Status.md) - [Workspace](docs/Workspace.md) - [WorkspaceDTO](docs/WorkspaceDTO.md) diff --git a/pkg/apiclient/api/openapi.yaml b/pkg/apiclient/api/openapi.yaml index a2d386d6c5..8db9bbd616 100644 --- a/pkg/apiclient/api/openapi.yaml +++ b/pkg/apiclient/api/openapi.yaml @@ -1799,7 +1799,9 @@ components: providerId: providerId baseApiUrl: baseApiUrl alias: alias + signingKey: signingKey id: id + signingMethod: null token: token username: username properties: @@ -1811,6 +1813,10 @@ components: type: string providerId: type: string + signingKey: + type: string + signingMethod: + $ref: '#/components/schemas/SigningMethod' token: type: string username: @@ -2384,7 +2390,9 @@ components: providerId: providerId baseApiUrl: baseApiUrl alias: alias + signingKey: signingKey id: id + signingMethod: null token: token username: username properties: @@ -2396,6 +2404,10 @@ components: type: string providerId: type: string + signingKey: + type: string + signingMethod: + $ref: '#/components/schemas/SigningMethod' token: type: string username: @@ -2429,6 +2441,14 @@ components: required: - uptime type: object + SigningMethod: + enum: + - ssh + - gpg + type: string + x-enum-varnames: + - SigningMethodSSH + - SigningMethodGPG Status: enum: - Unmodified diff --git a/pkg/apiclient/docs/GitProvider.md b/pkg/apiclient/docs/GitProvider.md index bf09388d36..900554152f 100644 --- a/pkg/apiclient/docs/GitProvider.md +++ b/pkg/apiclient/docs/GitProvider.md @@ -8,6 +8,8 @@ Name | Type | Description | Notes **BaseApiUrl** | Pointer to **string** | | [optional] **Id** | **string** | | **ProviderId** | **string** | | +**SigningKey** | Pointer to **string** | | [optional] +**SigningMethod** | Pointer to [**SigningMethod**](SigningMethod.md) | | [optional] **Token** | **string** | | **Username** | **string** | | @@ -115,6 +117,56 @@ and a boolean to check if the value has been set. SetProviderId sets ProviderId field to given value. +### GetSigningKey + +`func (o *GitProvider) GetSigningKey() string` + +GetSigningKey returns the SigningKey field if non-nil, zero value otherwise. + +### GetSigningKeyOk + +`func (o *GitProvider) GetSigningKeyOk() (*string, bool)` + +GetSigningKeyOk returns a tuple with the SigningKey field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSigningKey + +`func (o *GitProvider) SetSigningKey(v string)` + +SetSigningKey sets SigningKey field to given value. + +### HasSigningKey + +`func (o *GitProvider) HasSigningKey() bool` + +HasSigningKey returns a boolean if a field has been set. + +### GetSigningMethod + +`func (o *GitProvider) GetSigningMethod() SigningMethod` + +GetSigningMethod returns the SigningMethod field if non-nil, zero value otherwise. + +### GetSigningMethodOk + +`func (o *GitProvider) GetSigningMethodOk() (*SigningMethod, bool)` + +GetSigningMethodOk returns a tuple with the SigningMethod field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSigningMethod + +`func (o *GitProvider) SetSigningMethod(v SigningMethod)` + +SetSigningMethod sets SigningMethod field to given value. + +### HasSigningMethod + +`func (o *GitProvider) HasSigningMethod() bool` + +HasSigningMethod returns a boolean if a field has been set. + ### GetToken `func (o *GitProvider) GetToken() string` diff --git a/pkg/apiclient/docs/SetGitProviderConfig.md b/pkg/apiclient/docs/SetGitProviderConfig.md index 21ba99e4cd..5de81d22b1 100644 --- a/pkg/apiclient/docs/SetGitProviderConfig.md +++ b/pkg/apiclient/docs/SetGitProviderConfig.md @@ -8,6 +8,8 @@ Name | Type | Description | Notes **BaseApiUrl** | Pointer to **string** | | [optional] **Id** | Pointer to **string** | | [optional] **ProviderId** | **string** | | +**SigningKey** | Pointer to **string** | | [optional] +**SigningMethod** | Pointer to [**SigningMethod**](SigningMethod.md) | | [optional] **Token** | **string** | | **Username** | Pointer to **string** | | [optional] @@ -125,6 +127,56 @@ and a boolean to check if the value has been set. SetProviderId sets ProviderId field to given value. +### GetSigningKey + +`func (o *SetGitProviderConfig) GetSigningKey() string` + +GetSigningKey returns the SigningKey field if non-nil, zero value otherwise. + +### GetSigningKeyOk + +`func (o *SetGitProviderConfig) GetSigningKeyOk() (*string, bool)` + +GetSigningKeyOk returns a tuple with the SigningKey field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSigningKey + +`func (o *SetGitProviderConfig) SetSigningKey(v string)` + +SetSigningKey sets SigningKey field to given value. + +### HasSigningKey + +`func (o *SetGitProviderConfig) HasSigningKey() bool` + +HasSigningKey returns a boolean if a field has been set. + +### GetSigningMethod + +`func (o *SetGitProviderConfig) GetSigningMethod() SigningMethod` + +GetSigningMethod returns the SigningMethod field if non-nil, zero value otherwise. + +### GetSigningMethodOk + +`func (o *SetGitProviderConfig) GetSigningMethodOk() (*SigningMethod, bool)` + +GetSigningMethodOk returns a tuple with the SigningMethod field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSigningMethod + +`func (o *SetGitProviderConfig) SetSigningMethod(v SigningMethod)` + +SetSigningMethod sets SigningMethod field to given value. + +### HasSigningMethod + +`func (o *SetGitProviderConfig) HasSigningMethod() bool` + +HasSigningMethod returns a boolean if a field has been set. + ### GetToken `func (o *SetGitProviderConfig) GetToken() string` diff --git a/pkg/apiclient/docs/SigningMethod.md b/pkg/apiclient/docs/SigningMethod.md new file mode 100644 index 0000000000..ddab2bd54f --- /dev/null +++ b/pkg/apiclient/docs/SigningMethod.md @@ -0,0 +1,13 @@ +# SigningMethod + +## Enum + + +* `SigningMethodSSH` (value: `"ssh"`) + +* `SigningMethodGPG` (value: `"gpg"`) + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/pkg/apiclient/model_git_provider.go b/pkg/apiclient/model_git_provider.go index 25a322386e..cad2d0906a 100644 --- a/pkg/apiclient/model_git_provider.go +++ b/pkg/apiclient/model_git_provider.go @@ -21,12 +21,14 @@ var _ MappedNullable = &GitProvider{} // GitProvider struct for GitProvider type GitProvider struct { - Alias string `json:"alias"` - BaseApiUrl *string `json:"baseApiUrl,omitempty"` - Id string `json:"id"` - ProviderId string `json:"providerId"` - Token string `json:"token"` - Username string `json:"username"` + Alias string `json:"alias"` + BaseApiUrl *string `json:"baseApiUrl,omitempty"` + Id string `json:"id"` + ProviderId string `json:"providerId"` + SigningKey *string `json:"signingKey,omitempty"` + SigningMethod *SigningMethod `json:"signingMethod,omitempty"` + Token string `json:"token"` + Username string `json:"username"` } type _GitProvider GitProvider @@ -157,6 +159,70 @@ func (o *GitProvider) SetProviderId(v string) { o.ProviderId = v } +// GetSigningKey returns the SigningKey field value if set, zero value otherwise. +func (o *GitProvider) GetSigningKey() string { + if o == nil || IsNil(o.SigningKey) { + var ret string + return ret + } + return *o.SigningKey +} + +// GetSigningKeyOk returns a tuple with the SigningKey field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GitProvider) GetSigningKeyOk() (*string, bool) { + if o == nil || IsNil(o.SigningKey) { + return nil, false + } + return o.SigningKey, true +} + +// HasSigningKey returns a boolean if a field has been set. +func (o *GitProvider) HasSigningKey() bool { + if o != nil && !IsNil(o.SigningKey) { + return true + } + + return false +} + +// SetSigningKey gets a reference to the given string and assigns it to the SigningKey field. +func (o *GitProvider) SetSigningKey(v string) { + o.SigningKey = &v +} + +// GetSigningMethod returns the SigningMethod field value if set, zero value otherwise. +func (o *GitProvider) GetSigningMethod() SigningMethod { + if o == nil || IsNil(o.SigningMethod) { + var ret SigningMethod + return ret + } + return *o.SigningMethod +} + +// GetSigningMethodOk returns a tuple with the SigningMethod field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GitProvider) GetSigningMethodOk() (*SigningMethod, bool) { + if o == nil || IsNil(o.SigningMethod) { + return nil, false + } + return o.SigningMethod, true +} + +// HasSigningMethod returns a boolean if a field has been set. +func (o *GitProvider) HasSigningMethod() bool { + if o != nil && !IsNil(o.SigningMethod) { + return true + } + + return false +} + +// SetSigningMethod gets a reference to the given SigningMethod and assigns it to the SigningMethod field. +func (o *GitProvider) SetSigningMethod(v SigningMethod) { + o.SigningMethod = &v +} + // GetToken returns the Token field value func (o *GitProvider) GetToken() string { if o == nil { @@ -221,6 +287,12 @@ func (o GitProvider) ToMap() (map[string]interface{}, error) { } toSerialize["id"] = o.Id toSerialize["providerId"] = o.ProviderId + if !IsNil(o.SigningKey) { + toSerialize["signingKey"] = o.SigningKey + } + if !IsNil(o.SigningMethod) { + toSerialize["signingMethod"] = o.SigningMethod + } toSerialize["token"] = o.Token toSerialize["username"] = o.Username return toSerialize, nil diff --git a/pkg/apiclient/model_set_git_provider_config.go b/pkg/apiclient/model_set_git_provider_config.go index f55438f12b..34c45cbe2c 100644 --- a/pkg/apiclient/model_set_git_provider_config.go +++ b/pkg/apiclient/model_set_git_provider_config.go @@ -21,12 +21,14 @@ var _ MappedNullable = &SetGitProviderConfig{} // SetGitProviderConfig struct for SetGitProviderConfig type SetGitProviderConfig struct { - Alias *string `json:"alias,omitempty"` - BaseApiUrl *string `json:"baseApiUrl,omitempty"` - Id *string `json:"id,omitempty"` - ProviderId string `json:"providerId"` - Token string `json:"token"` - Username *string `json:"username,omitempty"` + Alias *string `json:"alias,omitempty"` + BaseApiUrl *string `json:"baseApiUrl,omitempty"` + Id *string `json:"id,omitempty"` + ProviderId string `json:"providerId"` + SigningKey *string `json:"signingKey,omitempty"` + SigningMethod *SigningMethod `json:"signingMethod,omitempty"` + Token string `json:"token"` + Username *string `json:"username,omitempty"` } type _SetGitProviderConfig SetGitProviderConfig @@ -170,6 +172,70 @@ func (o *SetGitProviderConfig) SetProviderId(v string) { o.ProviderId = v } +// GetSigningKey returns the SigningKey field value if set, zero value otherwise. +func (o *SetGitProviderConfig) GetSigningKey() string { + if o == nil || IsNil(o.SigningKey) { + var ret string + return ret + } + return *o.SigningKey +} + +// GetSigningKeyOk returns a tuple with the SigningKey field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SetGitProviderConfig) GetSigningKeyOk() (*string, bool) { + if o == nil || IsNil(o.SigningKey) { + return nil, false + } + return o.SigningKey, true +} + +// HasSigningKey returns a boolean if a field has been set. +func (o *SetGitProviderConfig) HasSigningKey() bool { + if o != nil && !IsNil(o.SigningKey) { + return true + } + + return false +} + +// SetSigningKey gets a reference to the given string and assigns it to the SigningKey field. +func (o *SetGitProviderConfig) SetSigningKey(v string) { + o.SigningKey = &v +} + +// GetSigningMethod returns the SigningMethod field value if set, zero value otherwise. +func (o *SetGitProviderConfig) GetSigningMethod() SigningMethod { + if o == nil || IsNil(o.SigningMethod) { + var ret SigningMethod + return ret + } + return *o.SigningMethod +} + +// GetSigningMethodOk returns a tuple with the SigningMethod field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SetGitProviderConfig) GetSigningMethodOk() (*SigningMethod, bool) { + if o == nil || IsNil(o.SigningMethod) { + return nil, false + } + return o.SigningMethod, true +} + +// HasSigningMethod returns a boolean if a field has been set. +func (o *SetGitProviderConfig) HasSigningMethod() bool { + if o != nil && !IsNil(o.SigningMethod) { + return true + } + + return false +} + +// SetSigningMethod gets a reference to the given SigningMethod and assigns it to the SigningMethod field. +func (o *SetGitProviderConfig) SetSigningMethod(v SigningMethod) { + o.SigningMethod = &v +} + // GetToken returns the Token field value func (o *SetGitProviderConfig) GetToken() string { if o == nil { @@ -246,6 +312,12 @@ func (o SetGitProviderConfig) ToMap() (map[string]interface{}, error) { toSerialize["id"] = o.Id } toSerialize["providerId"] = o.ProviderId + if !IsNil(o.SigningKey) { + toSerialize["signingKey"] = o.SigningKey + } + if !IsNil(o.SigningMethod) { + toSerialize["signingMethod"] = o.SigningMethod + } toSerialize["token"] = o.Token if !IsNil(o.Username) { toSerialize["username"] = o.Username diff --git a/pkg/apiclient/model_signing_method.go b/pkg/apiclient/model_signing_method.go new file mode 100644 index 0000000000..43a022968e --- /dev/null +++ b/pkg/apiclient/model_signing_method.go @@ -0,0 +1,110 @@ +/* +Daytona Server API + +Daytona Server API + +API version: v0.0.0-dev +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package apiclient + +import ( + "encoding/json" + "fmt" +) + +// SigningMethod the model 'SigningMethod' +type SigningMethod string + +// List of SigningMethod +const ( + SigningMethodSSH SigningMethod = "ssh" + SigningMethodGPG SigningMethod = "gpg" +) + +// All allowed values of SigningMethod enum +var AllowedSigningMethodEnumValues = []SigningMethod{ + "ssh", + "gpg", +} + +func (v *SigningMethod) UnmarshalJSON(src []byte) error { + var value string + err := json.Unmarshal(src, &value) + if err != nil { + return err + } + enumTypeValue := SigningMethod(value) + for _, existing := range AllowedSigningMethodEnumValues { + if existing == enumTypeValue { + *v = enumTypeValue + return nil + } + } + + return fmt.Errorf("%+v is not a valid SigningMethod", value) +} + +// NewSigningMethodFromValue returns a pointer to a valid SigningMethod +// for the value passed as argument, or an error if the value passed is not allowed by the enum +func NewSigningMethodFromValue(v string) (*SigningMethod, error) { + ev := SigningMethod(v) + if ev.IsValid() { + return &ev, nil + } else { + return nil, fmt.Errorf("invalid value '%v' for SigningMethod: valid values are %v", v, AllowedSigningMethodEnumValues) + } +} + +// IsValid return true if the value is valid for the enum, false otherwise +func (v SigningMethod) IsValid() bool { + for _, existing := range AllowedSigningMethodEnumValues { + if existing == v { + return true + } + } + return false +} + +// Ptr returns reference to SigningMethod value +func (v SigningMethod) Ptr() *SigningMethod { + return &v +} + +type NullableSigningMethod struct { + value *SigningMethod + isSet bool +} + +func (v NullableSigningMethod) Get() *SigningMethod { + return v.value +} + +func (v *NullableSigningMethod) Set(val *SigningMethod) { + v.value = val + v.isSet = true +} + +func (v NullableSigningMethod) IsSet() bool { + return v.isSet +} + +func (v *NullableSigningMethod) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSigningMethod(val *SigningMethod) *NullableSigningMethod { + return &NullableSigningMethod{value: val, isSet: true} +} + +func (v NullableSigningMethod) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSigningMethod) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/pkg/cmd/gitprovider/list.go b/pkg/cmd/gitprovider/list.go index c8878bb469..0509525d47 100644 --- a/pkg/cmd/gitprovider/list.go +++ b/pkg/cmd/gitprovider/list.go @@ -53,6 +53,10 @@ var gitProviderListCmd = &cobra.Command{ gitProviderView.BaseApiUrl = *gitProvider.BaseApiUrl } + if gitProvider.SigningMethod != nil { + gitProviderView.SigningMethod = string(*gitProvider.SigningMethod) + } + gitProviderViewList = append(gitProviderViewList, gitProviderView) } } diff --git a/pkg/cmd/gitprovider/update.go b/pkg/cmd/gitprovider/update.go index bc0d82122d..b713cb288d 100644 --- a/pkg/cmd/gitprovider/update.go +++ b/pkg/cmd/gitprovider/update.go @@ -47,12 +47,14 @@ var gitProviderUpdateCmd = &cobra.Command{ }) setGitProviderConfig := apiclient.SetGitProviderConfig{ - Id: &selectedGitProvider.Id, - ProviderId: selectedGitProvider.ProviderId, - Token: selectedGitProvider.Token, - BaseApiUrl: selectedGitProvider.BaseApiUrl, - Username: &selectedGitProvider.Username, - Alias: &selectedGitProvider.Alias, + Id: &selectedGitProvider.Id, + ProviderId: selectedGitProvider.ProviderId, + Token: selectedGitProvider.Token, + BaseApiUrl: selectedGitProvider.BaseApiUrl, + Username: &selectedGitProvider.Username, + Alias: &selectedGitProvider.Alias, + SigningMethod: selectedGitProvider.SigningMethod, + SigningKey: selectedGitProvider.SigningKey, } err = gitprovider_view.GitProviderCreationView(ctx, apiClient, &setGitProviderConfig, existingAliases) diff --git a/pkg/cmd/workspace/code.go b/pkg/cmd/workspace/code.go index 65a7f68f64..d231e5f14f 100644 --- a/pkg/cmd/workspace/code.go +++ b/pkg/cmd/workspace/code.go @@ -41,6 +41,7 @@ var CodeCmd = &cobra.Command{ ctx := context.Background() var workspaceId string var projectName string + var providerConfigId *string var ideId string var workspace *apiclient.WorkspaceDTO @@ -87,11 +88,19 @@ var CodeCmd = &cobra.Command{ if selectedProject == nil { return nil } + projectName = selectedProject.Name + providerConfigId = selectedProject.GitProviderConfigId } if len(args) == 2 { projectName = args[1] + for _, project := range workspace.Projects { + if project.Name == projectName { + providerConfigId = project.GitProviderConfigId + break + } + } } if ideFlag != "" { @@ -116,10 +125,15 @@ var CodeCmd = &cobra.Command{ } } + gpgKey, err := GetGitProviderGpgKey(apiClient, ctx, providerConfigId) + if err != nil { + log.Warn(err) + } + yesFlag, _ := cmd.Flags().GetBool("yes") ideList := config.GetIdeList() ide_views.RenderIdeOpeningMessage(workspace.Name, projectName, ideId, ideList) - return openIDE(ideId, activeProfile, workspaceId, projectName, providerMetadata, yesFlag) + return openIDE(ideId, activeProfile, workspaceId, projectName, providerMetadata, yesFlag, gpgKey) }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) >= 2 { @@ -159,26 +173,26 @@ func selectWorkspaceProject(workspaceId string, profile *config.Profile) (*apicl return nil, errors.New("no projects found in workspace") } -func openIDE(ideId string, activeProfile config.Profile, workspaceId string, projectName string, projectProviderMetadata string, yesFlag bool) error { +func openIDE(ideId string, activeProfile config.Profile, workspaceId string, projectName string, projectProviderMetadata string, yesFlag bool, gpgKey string) error { telemetry.AdditionalData["ide"] = ideId switch ideId { case "vscode": - return ide.OpenVSCode(activeProfile, workspaceId, projectName, projectProviderMetadata) + return ide.OpenVSCode(activeProfile, workspaceId, projectName, projectProviderMetadata, gpgKey) case "ssh": - return ide.OpenTerminalSsh(activeProfile, workspaceId, projectName) + return ide.OpenTerminalSsh(activeProfile, workspaceId, projectName, gpgKey) case "browser": - return ide.OpenBrowserIDE(activeProfile, workspaceId, projectName, projectProviderMetadata) + return ide.OpenBrowserIDE(activeProfile, workspaceId, projectName, projectProviderMetadata, gpgKey) case "cursor": - return ide.OpenCursor(activeProfile, workspaceId, projectName, projectProviderMetadata) + return ide.OpenCursor(activeProfile, workspaceId, projectName, projectProviderMetadata, gpgKey) case "jupyter": - return ide.OpenJupyterIDE(activeProfile, workspaceId, projectName, projectProviderMetadata, yesFlag) + return ide.OpenJupyterIDE(activeProfile, workspaceId, projectName, projectProviderMetadata, yesFlag, gpgKey) case "fleet": - return ide.OpenFleet(activeProfile, workspaceId, projectName) + return ide.OpenFleet(activeProfile, workspaceId, projectName, gpgKey) default: _, ok := jetbrains.GetIdes()[jetbrains.Id(ideId)] if ok { - return ide.OpenJetbrainsIDE(activeProfile, ideId, workspaceId, projectName) + return ide.OpenJetbrainsIDE(activeProfile, ideId, workspaceId, projectName, gpgKey) } } diff --git a/pkg/cmd/workspace/create.go b/pkg/cmd/workspace/create.go index 3ea177feb4..f0c578b9ff 100644 --- a/pkg/cmd/workspace/create.go +++ b/pkg/cmd/workspace/create.go @@ -20,6 +20,7 @@ import ( "github.com/daytonaio/daytona/pkg/apiclient" workspace_util "github.com/daytonaio/daytona/pkg/cmd/workspace/util" "github.com/daytonaio/daytona/pkg/common" + "github.com/daytonaio/daytona/pkg/gitprovider" "github.com/daytonaio/daytona/pkg/logs" "github.com/daytonaio/daytona/pkg/views" logs_view "github.com/daytonaio/daytona/pkg/views/logs" @@ -29,6 +30,7 @@ import ( "github.com/daytonaio/daytona/pkg/views/workspace/selection" "github.com/daytonaio/daytona/pkg/workspace/project" "github.com/docker/docker/pkg/stringid" + log "github.com/sirupsen/logrus" "tailscale.com/tsnet" "github.com/spf13/cobra" @@ -170,8 +172,12 @@ var CreateCmd = &cobra.Command{ stopLogs() return apiclient_util.HandleErrorResponse(res, err) } + gpgKey, err := GetGitProviderGpgKey(apiClient, ctx, projects[0].GitProviderConfigId) + if err != nil { + log.Warn(err) + } - err = waitForDial(createdWorkspace, &activeProfile, tsConn) + err = waitForDial(createdWorkspace, &activeProfile, tsConn, gpgKey) if err != nil { stopLogs() return err @@ -217,7 +223,7 @@ var CreateCmd = &cobra.Command{ return err } - return openIDE(chosenIdeId, activeProfile, createdWorkspace.Id, wsInfo.Projects[0].Name, providerMetadata, yesFlag) + return openIDE(chosenIdeId, activeProfile, createdWorkspace.Id, wsInfo.Projects[0].Name, providerMetadata, yesFlag, gpgKey) }, } @@ -441,9 +447,9 @@ func processGitURL(ctx context.Context, repoUrl string, apiClient *apiclient.API return nil, nil } -func waitForDial(workspace *apiclient.Workspace, activeProfile *config.Profile, tsConn *tsnet.Server) error { +func waitForDial(workspace *apiclient.Workspace, activeProfile *config.Profile, tsConn *tsnet.Server, gpgKey string) error { if workspace.Target == "local" && (activeProfile != nil && activeProfile.Id == "default") { - err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspace.Id, workspace.Projects[0].Name) + err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspace.Id, workspace.Projects[0].Name, gpgKey) if err != nil { return err } @@ -508,3 +514,33 @@ func dedupProjectNames(projects *[]apiclient.CreateProjectDTO) { } } } + +func GetGitProviderGpgKey(apiClient *apiclient.APIClient, ctx context.Context, providerConfigId *string) (string, error) { + if providerConfigId == nil { + return "", nil + } + + var providerConfig *gitprovider.GitProviderConfig + var gpgKey string + + gitProvider, res, err := apiClient.GitProviderAPI.GetGitProvider(ctx, *providerConfigId).Execute() + if err != nil { + return "", apiclient_util.HandleErrorResponse(res, err) + } + + // Extract GPG key if present + if gitProvider != nil { + providerConfig = &gitprovider.GitProviderConfig{ + SigningMethod: (*gitprovider.SigningMethod)(gitProvider.SigningMethod), + SigningKey: gitProvider.SigningKey, + } + + if providerConfig.SigningMethod != nil && providerConfig.SigningKey != nil { + if *providerConfig.SigningMethod == gitprovider.SigningMethodGPG { + gpgKey = *providerConfig.SigningKey + } + } + } + + return gpgKey, nil +} diff --git a/pkg/cmd/workspace/ssh.go b/pkg/cmd/workspace/ssh.go index 775b0d3b7b..dd8e3dbc34 100644 --- a/pkg/cmd/workspace/ssh.go +++ b/pkg/cmd/workspace/ssh.go @@ -13,6 +13,7 @@ import ( workspace_util "github.com/daytonaio/daytona/pkg/cmd/workspace/util" "github.com/daytonaio/daytona/pkg/ide" "github.com/daytonaio/daytona/pkg/views/workspace/selection" + log "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -36,6 +37,7 @@ var SshCmd = &cobra.Command{ ctx := context.Background() var workspace *apiclient.WorkspaceDTO var projectName string + var providerConfigId *string apiClient, err := apiclient_util.GetApiClient(&activeProfile) if err != nil { @@ -68,10 +70,17 @@ var SshCmd = &cobra.Command{ return nil } projectName = selectedProject.Name + providerConfigId = selectedProject.GitProviderConfigId } if len(args) >= 2 { projectName = args[1] + for _, project := range workspace.Projects { + if project.Name == projectName { + providerConfigId = project.GitProviderConfigId + break + } + } } if !workspace_util.IsProjectRunning(workspace, projectName) { @@ -89,7 +98,12 @@ var SshCmd = &cobra.Command{ sshArgs = append(sshArgs, args[2:]...) } - return ide.OpenTerminalSsh(activeProfile, workspace.Id, projectName, sshArgs...) + gpgKey, err := GetGitProviderGpgKey(apiClient, ctx, providerConfigId) + if err != nil { + log.Warn(err) + } + + return ide.OpenTerminalSsh(activeProfile, workspace.Id, projectName, gpgKey, sshArgs...) }, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) >= 2 { diff --git a/pkg/cmd/workspace/start.go b/pkg/cmd/workspace/start.go index 88c3395438..7d4b1dd7df 100644 --- a/pkg/cmd/workspace/start.go +++ b/pkg/cmd/workspace/start.go @@ -42,6 +42,7 @@ var StartCmd = &cobra.Command{ var activeProfile config.Profile var ideId string var ideList []config.Ide + var providerConfigId *string projectProviderMetadata := "" ctx := context.Background() @@ -96,7 +97,16 @@ var StartCmd = &cobra.Command{ workspaceId = wsInfo.Id if startProjectFlag == "" { startProjectFlag = wsInfo.Projects[0].Name + providerConfigId = wsInfo.Projects[0].GitProviderConfigId + } else { + for _, project := range wsInfo.Projects { + if project.Name == startProjectFlag { + providerConfigId = project.GitProviderConfigId + break + } + } } + if ideId != "ssh" { projectProviderMetadata, err = workspace_util.GetProjectProviderMetadata(wsInfo, wsInfo.Projects[0].Name) if err != nil { @@ -109,6 +119,10 @@ var StartCmd = &cobra.Command{ if err != nil { return err } + gpgKey, err := GetGitProviderGpgKey(apiClient, ctx, providerConfigId) + if err != nil { + log.Warn(err) + } if startProjectFlag == "" { views.RenderInfoMessage(fmt.Sprintf("Workspace '%s' started successfully", workspaceName)) @@ -117,7 +131,7 @@ var StartCmd = &cobra.Command{ if codeFlag { ide_views.RenderIdeOpeningMessage(workspaceName, startProjectFlag, ideId, ideList) - err = openIDE(ideId, activeProfile, workspaceId, startProjectFlag, projectProviderMetadata, yesFlag) + err = openIDE(ideId, activeProfile, workspaceId, startProjectFlag, projectProviderMetadata, yesFlag, gpgKey) if err != nil { return err } diff --git a/pkg/db/dto/git_provider.go b/pkg/db/dto/git_provider.go index 4167b0772f..d08bb586e7 100644 --- a/pkg/db/dto/git_provider.go +++ b/pkg/db/dto/git_provider.go @@ -8,22 +8,26 @@ import ( ) type GitProviderConfigDTO struct { - Id string `gorm:"primaryKey"` - ProviderId string `json:"providerId"` - Username string `json:"username"` - Token string `json:"token"` - BaseApiUrl *string `json:"baseApiUrl,omitempty"` - Alias string `gorm:"uniqueIndex" json:"alias"` + Id string `gorm:"primaryKey"` + ProviderId string `json:"providerId"` + Username string `json:"username"` + Token string `json:"token"` + BaseApiUrl *string `json:"baseApiUrl,omitempty"` + Alias string `gorm:"uniqueIndex" json:"alias"` + SigningKey *string `json:"siginingKey,omitempty"` + SigningMethod *gitprovider.SigningMethod `json:"siginingMethod,omitempty"` } func ToGitProviderConfigDTO(gitProvider gitprovider.GitProviderConfig) GitProviderConfigDTO { gitProviderDTO := GitProviderConfigDTO{ - Id: gitProvider.Id, - ProviderId: gitProvider.ProviderId, - Username: gitProvider.Username, - Token: gitProvider.Token, - BaseApiUrl: gitProvider.BaseApiUrl, - Alias: gitProvider.Alias, + Id: gitProvider.Id, + ProviderId: gitProvider.ProviderId, + Username: gitProvider.Username, + Token: gitProvider.Token, + BaseApiUrl: gitProvider.BaseApiUrl, + Alias: gitProvider.Alias, + SigningKey: gitProvider.SigningKey, + SigningMethod: gitProvider.SigningMethod, } return gitProviderDTO @@ -31,11 +35,13 @@ func ToGitProviderConfigDTO(gitProvider gitprovider.GitProviderConfig) GitProvid func ToGitProviderConfig(gitProviderDTO GitProviderConfigDTO) gitprovider.GitProviderConfig { return gitprovider.GitProviderConfig{ - Id: gitProviderDTO.Id, - ProviderId: gitProviderDTO.ProviderId, - Username: gitProviderDTO.Username, - Token: gitProviderDTO.Token, - BaseApiUrl: gitProviderDTO.BaseApiUrl, - Alias: gitProviderDTO.Alias, + Id: gitProviderDTO.Id, + ProviderId: gitProviderDTO.ProviderId, + Username: gitProviderDTO.Username, + Token: gitProviderDTO.Token, + BaseApiUrl: gitProviderDTO.BaseApiUrl, + Alias: gitProviderDTO.Alias, + SigningKey: gitProviderDTO.SigningKey, + SigningMethod: gitProviderDTO.SigningMethod, } } diff --git a/pkg/git/service.go b/pkg/git/service.go index 1b86684546..0b4aa29345 100644 --- a/pkg/git/service.go +++ b/pkg/git/service.go @@ -38,7 +38,7 @@ type IGitService interface { CloneRepository(repo *gitprovider.GitRepository, auth *http.BasicAuth) error CloneRepositoryCmd(repo *gitprovider.GitRepository, auth *http.BasicAuth) []string RepositoryExists() (bool, error) - SetGitConfig(userData *gitprovider.GitUser) error + SetGitConfig(userData *gitprovider.GitUser, providerConfig *gitprovider.GitProviderConfig) error GetGitStatus() (*project.GitStatus, error) } @@ -132,7 +132,7 @@ func (s *Service) RepositoryExists() (bool, error) { return true, nil } -func (s *Service) SetGitConfig(userData *gitprovider.GitUser) error { +func (s *Service) SetGitConfig(userData *gitprovider.GitUser, providerConfig *gitprovider.GitProviderConfig) error { gitConfigFileName := s.GitConfigFileName var gitConfigContent []byte @@ -188,17 +188,106 @@ func (s *Service) SetGitConfig(userData *gitprovider.GitUser) error { } } + if err := s.setSigningConfig(cfg, providerConfig, userData); err != nil { + return err + } + var buf bytes.Buffer _, err = cfg.WriteTo(&buf) if err != nil { return err } - err = os.WriteFile(gitConfigFileName, buf.Bytes(), 0644) + return os.WriteFile(gitConfigFileName, buf.Bytes(), 0644) +} + +func (s *Service) setSigningConfig(cfg *ini.File, providerConfig *gitprovider.GitProviderConfig, userData *gitprovider.GitUser) error { + if providerConfig == nil || providerConfig.SigningMethod == nil || providerConfig.SigningKey == nil { + return nil + } + + if !cfg.HasSection("user") { + _, err := cfg.NewSection("user") + if err != nil { + return err + } + } + + _, err := cfg.Section("user").NewKey("signingkey", *providerConfig.SigningKey) if err != nil { return err } + if !cfg.HasSection("commit") { + _, err := cfg.NewSection("commit") + if err != nil { + return err + } + } + + switch *providerConfig.SigningMethod { + case gitprovider.SigningMethodGPG: + _, err := cfg.Section("commit").NewKey("gpgSign", "true") + if err != nil { + return err + } + case gitprovider.SigningMethodSSH: + err := s.configureAllowedSigners(userData.Email, *providerConfig.SigningKey) + if err != nil { + return err + } + + if !cfg.HasSection("gpg") { + _, err := cfg.NewSection("gpg") + if err != nil { + return err + } + } + _, err = cfg.Section("gpg").NewKey("format", "ssh") + if err != nil { + return err + } + + if !cfg.HasSection("gpg \"ssh\"") { + _, err := cfg.NewSection("gpg \"ssh\"") + if err != nil { + return err + } + } + + allowedSignersFile := filepath.Join(os.Getenv("HOME"), ".ssh/allowed_signers") + _, err = cfg.Section("gpg \"ssh\"").NewKey("allowedSignersFile", allowedSignersFile) + if err != nil { + return err + } + } + return nil +} + +func (s *Service) configureAllowedSigners(email, sshKey string) error { + homeDir := os.Getenv("HOME") + sshDir := filepath.Join(homeDir, ".ssh") + allowedSignersFile := filepath.Join(sshDir, "allowed_signers") + + err := os.MkdirAll(sshDir, 0700) + if err != nil { + return fmt.Errorf("failed to create SSH directory: %w", err) + } + + entry := fmt.Sprintf("%s namespaces=\"git\" %s\n", email, sshKey) + + existingContent, err := os.ReadFile(allowedSignersFile) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to read allowed_signers file: %w", err) + } + + newContent := string(existingContent) + entry + + err = os.WriteFile(allowedSignersFile, []byte(newContent), 0600) + if err != nil { + return fmt.Errorf("failed to write to allowed_signers file: %w", err) + } + return nil } diff --git a/pkg/gitprovider/types.go b/pkg/gitprovider/types.go index c88444d959..5b38d2e310 100644 --- a/pkg/gitprovider/types.go +++ b/pkg/gitprovider/types.go @@ -3,13 +3,22 @@ package gitprovider +type SigningMethod string // @name SigningMethod + +const ( + SigningMethodSSH SigningMethod = "ssh" + SigningMethodGPG SigningMethod = "gpg" +) + type GitProviderConfig struct { - Id string `json:"id" validate:"required"` - ProviderId string `json:"providerId" validate:"required"` - Username string `json:"username" validate:"required"` - BaseApiUrl *string `json:"baseApiUrl,omitempty" validate:"optional"` - Token string `json:"token" validate:"required"` - Alias string `json:"alias" validate:"required"` + Id string `json:"id" validate:"required"` + ProviderId string `json:"providerId" validate:"required"` + Username string `json:"username" validate:"required"` + BaseApiUrl *string `json:"baseApiUrl,omitempty" validate:"optional"` + Token string `json:"token" validate:"required"` + Alias string `json:"alias" validate:"required"` + SigningKey *string `json:"signingKey,omitempty" validate:"optional"` + SigningMethod *SigningMethod `json:"signingMethod,omitempty" validate:"optional"` } // @name GitProvider type GitUser struct { diff --git a/pkg/ide/browser.go b/pkg/ide/browser.go index 9d7ffacbde..1ad9c99382 100644 --- a/pkg/ide/browser.go +++ b/pkg/ide/browser.go @@ -23,9 +23,9 @@ import ( const startVSCodeServerCommand = "$HOME/vscode-server/bin/openvscode-server --start-server --port=63000 --host=0.0.0.0 --without-connection-token --disable-workspace-trust --default-folder=" -func OpenBrowserIDE(activeProfile config.Profile, workspaceId string, projectName string, projectProviderMetadata string) error { +func OpenBrowserIDE(activeProfile config.Profile, workspaceId string, projectName string, projectProviderMetadata string, gpgKey string) error { // Download and start IDE - err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName) + err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName, gpgKey) if err != nil { return err } @@ -42,7 +42,7 @@ func OpenBrowserIDE(activeProfile config.Profile, workspaceId string, projectNam return err } - projectDir, err := util.GetProjectDir(activeProfile, workspaceId, projectName) + projectDir, err := util.GetProjectDir(activeProfile, workspaceId, projectName, gpgKey) if err != nil { return err } diff --git a/pkg/ide/cursor.go b/pkg/ide/cursor.go index a6c678ff01..00e63fbc58 100644 --- a/pkg/ide/cursor.go +++ b/pkg/ide/cursor.go @@ -14,7 +14,7 @@ import ( "github.com/daytonaio/daytona/pkg/build/devcontainer" ) -func OpenCursor(activeProfile config.Profile, workspaceId string, projectName string, projectProviderMetadata string) error { +func OpenCursor(activeProfile config.Profile, workspaceId string, projectName string, projectProviderMetadata string, gpgkey string) error { path, err := GetCursorBinaryPath() if err != nil { return err @@ -22,7 +22,7 @@ func OpenCursor(activeProfile config.Profile, workspaceId string, projectName st projectHostname := config.GetProjectHostname(activeProfile.Id, workspaceId, projectName) - projectDir, err := util.GetProjectDir(activeProfile, workspaceId, projectName) + projectDir, err := util.GetProjectDir(activeProfile, workspaceId, projectName, gpgkey) if err != nil { return err } diff --git a/pkg/ide/fleet.go b/pkg/ide/fleet.go index c8ac5187dd..4f39157d1f 100644 --- a/pkg/ide/fleet.go +++ b/pkg/ide/fleet.go @@ -13,13 +13,13 @@ import ( log "github.com/sirupsen/logrus" ) -func OpenFleet(activeProfile config.Profile, workspaceId string, projectName string) error { +func OpenFleet(activeProfile config.Profile, workspaceId string, projectName string, gpgKey string) error { if err := CheckFleetInstallation(); err != nil { return err } projectHostname := config.GetProjectHostname(activeProfile.Id, workspaceId, projectName) - projectDir, err := util.GetProjectDir(activeProfile, workspaceId, projectName) + projectDir, err := util.GetProjectDir(activeProfile, workspaceId, projectName, gpgKey) if err != nil { return err } diff --git a/pkg/ide/jetbrains.go b/pkg/ide/jetbrains.go index 7b9b8be024..4ddc570765 100644 --- a/pkg/ide/jetbrains.go +++ b/pkg/ide/jetbrains.go @@ -23,13 +23,13 @@ import ( "github.com/pkg/browser" ) -func OpenJetbrainsIDE(activeProfile config.Profile, ide, workspaceId, projectName string) error { +func OpenJetbrainsIDE(activeProfile config.Profile, ide, workspaceId, projectName string, gpgKey string) error { err := IsJetBrainsGatewayInstalled() if err != nil { return err } - projectDir, err := util.GetProjectDir(activeProfile, workspaceId, projectName) + projectDir, err := util.GetProjectDir(activeProfile, workspaceId, projectName, gpgKey) if err != nil { return err } @@ -41,7 +41,7 @@ func OpenJetbrainsIDE(activeProfile config.Profile, ide, workspaceId, projectNam return errors.New("IDE not found") } - home, err := util.GetHomeDir(activeProfile, workspaceId, projectName) + home, err := util.GetHomeDir(activeProfile, workspaceId, projectName, gpgKey) if err != nil { return err } diff --git a/pkg/ide/jupyter.go b/pkg/ide/jupyter.go index bf9bfb5bbb..c60ade2f17 100644 --- a/pkg/ide/jupyter.go +++ b/pkg/ide/jupyter.go @@ -26,9 +26,9 @@ import ( const startJupyterCommand = "notebook --no-browser --port=8888 --ip=0.0.0.0 --NotebookApp.token='' --NotebookApp.password=''" // OpenJupyterIDE manages the installation and startup of a Jupyter IDE on a remote workspace. -func OpenJupyterIDE(activeProfile config.Profile, workspaceId, projectName, projectProviderMetadata string, yesFlag bool) error { +func OpenJupyterIDE(activeProfile config.Profile, workspaceId, projectName, projectProviderMetadata string, yesFlag bool, gpgKey string) error { // Ensure SSH config entry is added - err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName) + err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName, gpgKey) if err != nil { return err } @@ -51,7 +51,7 @@ func OpenJupyterIDE(activeProfile config.Profile, workspaceId, projectName, proj } // Start Jupyter Notebook server - if err := startJupyterServer(projectHostname, activeProfile, workspaceId, projectName); err != nil { + if err := startJupyterServer(projectHostname, activeProfile, workspaceId, projectName, gpgKey); err != nil { return err } @@ -216,8 +216,8 @@ func ensureJupyterInstalled(hostname string) error { } // startJupyterServer starts the Jupyter Notebook server on the remote workspace. -func startJupyterServer(hostname string, activeProfile config.Profile, workspaceId, projectName string) error { - projectDir, err := util.GetProjectDir(activeProfile, workspaceId, projectName) +func startJupyterServer(hostname string, activeProfile config.Profile, workspaceId, projectName string, gpgKey string) error { + projectDir, err := util.GetProjectDir(activeProfile, workspaceId, projectName, gpgKey) if err != nil { return err } diff --git a/pkg/ide/terminal.go b/pkg/ide/terminal.go index e672e64026..ec2bf3aea4 100644 --- a/pkg/ide/terminal.go +++ b/pkg/ide/terminal.go @@ -10,8 +10,8 @@ import ( "github.com/daytonaio/daytona/cmd/daytona/config" ) -func OpenTerminalSsh(activeProfile config.Profile, workspaceId string, projectName string, args ...string) error { - err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName) +func OpenTerminalSsh(activeProfile config.Profile, workspaceId string, projectName string, gpgKey string, args ...string) error { + err := config.EnsureSshConfigEntryAdded(activeProfile.Id, workspaceId, projectName, gpgKey) if err != nil { return err } diff --git a/pkg/ide/vscode.go b/pkg/ide/vscode.go index 6952d0ed20..643359c853 100644 --- a/pkg/ide/vscode.go +++ b/pkg/ide/vscode.go @@ -19,7 +19,7 @@ import ( log "github.com/sirupsen/logrus" ) -func OpenVSCode(activeProfile config.Profile, workspaceId string, projectName string, projectProviderMetadata string) error { +func OpenVSCode(activeProfile config.Profile, workspaceId string, projectName string, projectProviderMetadata string, gpgKey string) error { CheckAndAlertVSCodeInstalled() err := installRemoteSSHExtension() if err != nil { @@ -28,7 +28,7 @@ func OpenVSCode(activeProfile config.Profile, workspaceId string, projectName st projectHostname := config.GetProjectHostname(activeProfile.Id, workspaceId, projectName) - projectDir, err := util.GetProjectDir(activeProfile, workspaceId, projectName) + projectDir, err := util.GetProjectDir(activeProfile, workspaceId, projectName, gpgKey) if err != nil { return err } diff --git a/pkg/views/gitprovider/info/view.go b/pkg/views/gitprovider/info/view.go index 164d5cc8d6..0ea4e11cae 100644 --- a/pkg/views/gitprovider/info/view.go +++ b/pkg/views/gitprovider/info/view.go @@ -40,6 +40,10 @@ func Render(gp *gitprovider.GitProviderView, forceUnstyled bool) { output += getInfoLine("Base API URL", gp.BaseApiUrl) + "\n" } + if gp.SigningMethod != "" { + output += getInfoLine("Signing Method", gp.SigningMethod) + "\n" + } + terminalWidth, _, err := term.GetSize(int(os.Stdout.Fd())) if err != nil { fmt.Println(output) diff --git a/pkg/views/gitprovider/list/view.go b/pkg/views/gitprovider/list/view.go index a953ef778d..d81d7e9a9b 100644 --- a/pkg/views/gitprovider/list/view.go +++ b/pkg/views/gitprovider/list/view.go @@ -13,20 +13,24 @@ import ( ) type rowData struct { - Name string - Alias string - Username string - BaseApiUrl string + Name string + Alias string + Username string + BaseApiUrl string + SigningMethod string } func ListGitProviders(gitProviderViewList []gitprovider.GitProviderView) { var showBaseApiUrlColumn bool - headers := []string{"Name", "Alias", "Username", "Base API URL"} + var showSigningMethodColumn bool + headers := []string{"Name", "Alias", "Username", "Base API URL", "Signing Method"} for _, gp := range gitProviderViewList { if gp.BaseApiUrl != "" { showBaseApiUrlColumn = true - break + } + if gp.SigningMethod != "" { + showSigningMethodColumn = true } } @@ -37,9 +41,15 @@ func ListGitProviders(gitProviderViewList []gitprovider.GitProviderView) { } if !showBaseApiUrlColumn { - headers = headers[:len(headers)-1] - for value := range data { - data[value] = data[value][:len(data[value])-1] + headers = removeHeader(headers, "Base API URL") + for i := range data { + data[i] = removeColumn(data[i], 3) + } + } + if !showSigningMethodColumn { + headers = removeHeader(headers, "Signing Method") + for i := range data { + data[i] = removeColumn(data[i], 4) } } @@ -50,6 +60,22 @@ func ListGitProviders(gitProviderViewList []gitprovider.GitProviderView) { fmt.Println(table) } +func removeHeader(headers []string, headerToRemove string) []string { + for i, header := range headers { + if header == headerToRemove { + return append(headers[:i], headers[i+1:]...) + } + } + return headers +} + +func removeColumn(data []string, index int) []string { + if index < 0 || index >= len(data) { + return data + } + return append(data[:index], data[index+1:]...) +} + func renderUnstyledList(gitProviderViewList []gitprovider.GitProviderView) { for _, b := range gitProviderViewList { info.Render(&b, true) @@ -67,11 +93,13 @@ func getRowFromRowData(build gitprovider.GitProviderView) []string { data.Alias = build.Alias data.Username = build.Username data.BaseApiUrl = build.BaseApiUrl + data.SigningMethod = build.SigningMethod return []string{ views.NameStyle.Render(data.Name), views.DefaultRowDataStyle.Render(data.Alias), views.DefaultRowDataStyle.Render(data.Username), views.DefaultRowDataStyle.Render(data.BaseApiUrl), + views.DefaultRowDataStyle.Render(data.SigningMethod), } } diff --git a/pkg/views/gitprovider/select.go b/pkg/views/gitprovider/select.go index e73732460e..0d67086864 100644 --- a/pkg/views/gitprovider/select.go +++ b/pkg/views/gitprovider/select.go @@ -7,6 +7,7 @@ import ( "context" "errors" "fmt" + "regexp" "slices" "github.com/charmbracelet/huh" @@ -64,6 +65,14 @@ func GitProviderCreationView(ctx context.Context, apiClient *apiclient.APIClient } } + var selectedSigningMethod string + var signingKey string + + if gitProviderAddView.SigningMethod != nil { + selectedSigningMethod = string(*gitProviderAddView.SigningMethod) + gitProviderAddView.SigningKey = nil + } + userDataForm := huh.NewForm( huh.NewGroup( huh.NewInput(). @@ -92,6 +101,7 @@ func GitProviderCreationView(ctx context.Context, apiClient *apiclient.APIClient ).WithHeight(6).WithHideFunc(func() bool { return !providerRequiresApiUrl(gitProviderAddView.ProviderId) }), + huh.NewGroup( huh.NewInput(). Title("Personal access token"). @@ -120,6 +130,43 @@ func GitProviderCreationView(ctx context.Context, apiClient *apiclient.APIClient return nil }), ).WithHeight(6), + + huh.NewGroup(huh.NewSelect[string](). + Title("Commit Signing Method"). + DescriptionFunc(func() string { + return getGitProviderSigningHelpMessage(gitProviderAddView.ProviderId) + }, nil). + Options( + huh.Option[string]{Key: "None", Value: "none"}, + huh.Option[string]{Key: "SSH", Value: "ssh"}, + huh.Option[string]{Key: "GPG", Value: "gpg"}, + ). + Value(&selectedSigningMethod).WithHeight(6), + ).WithHeight(8).WithHideFunc(func() bool { + return commitSigningNotSupported(gitProviderAddView.ProviderId) + }), + huh.NewGroup( + huh.NewInput(). + Title("Signing Key"). + Value(&signingKey). + DescriptionFunc(func() string { + return getSigningKeyDescription(selectedSigningMethod) + }, nil). + Validate(func(str string) error { + if selectedSigningMethod != "none" && str == "" { + return errors.New("signing key cannot be blank when a signing method is selected") + } + + if selectedSigningMethod == "ssh" { + if err := isValidSSHKey(str); err != nil { + return err + } + } + return nil + }), + ).WithHeight(5).WithHideFunc(func() bool { + return selectedSigningMethod == "none" + }), ).WithTheme(views.GetCustomTheme()) views.RenderInfoMessage(getGitProviderHelpMessage(gitProviderAddView.ProviderId)) @@ -128,6 +175,23 @@ func GitProviderCreationView(ctx context.Context, apiClient *apiclient.APIClient return err } + if selectedSigningMethod != "none" { + gitProviderAddView.SigningMethod = (*apiclient.SigningMethod)(&selectedSigningMethod) + gitProviderAddView.SigningKey = &signingKey + } else { + gitProviderAddView.SigningKey = nil + gitProviderAddView.SigningMethod = nil + } + + return nil + +} +func isValidSSHKey(key string) error { + sshKeyPattern := regexp.MustCompile(`^(ssh-(rsa|ed25519|dss|ecdsa-sha2-nistp(256|384|521)))\s+[A-Za-z0-9+/=]+(\s+.+)?$`) + if !sshKeyPattern.MatchString(key) { + return errors.New("invalid SSH key: must start with valid SSH key type (e.g., ssh-rsa, ssh-ed25519)") + } + return nil } @@ -139,6 +203,10 @@ func providerRequiresApiUrl(gitProviderId string) bool { return gitProviderId == "gitness" || gitProviderId == "github-enterprise-server" || gitProviderId == "gitlab-self-managed" || gitProviderId == "gitea" || gitProviderId == "bitbucket-server" || gitProviderId == "azure-devops" || gitProviderId == "aws-codecommit" } +func commitSigningNotSupported(gitProviderId string) bool { + return gitProviderId == "gitness" || gitProviderId == "bitbucket" || gitProviderId == "bitbucket-server" +} + func getApiUrlDescription(gitProviderId string) string { if gitProviderId == "gitlab-self-managed" { return "For example: http://gitlab-host/api/v4/" @@ -158,6 +226,17 @@ func getApiUrlDescription(gitProviderId string) string { return "" } +func getSigningKeyDescription(signingMethod string) string { + switch signingMethod { + case "gpg": + return "Provide your GPG key ID (e.g., 30F2B65B9246B6CA) for signing commits." + case "ssh": + return "Provide your public SSH key (e.g., ssh-ed25519 AAAAC3...) for secure signing." + default: + return "" + } +} + func getGitProviderHelpMessage(gitProviderId string) string { message := fmt.Sprintf("%s\n%s\n\n%s%s", lipgloss.NewStyle().Foreground(views.Green).Bold(true).Render("More information on:"), @@ -175,3 +254,12 @@ func getGitProviderHelpMessage(gitProviderId string) string { return message } + +func getGitProviderSigningHelpMessage(gitProviderId string) string { + signingDocsLink := config.GetDocsLinkForCommitSigning(gitProviderId) + + if signingDocsLink != "" { + return signingDocsLink + } + return "" +} diff --git a/pkg/views/gitprovider/types.go b/pkg/views/gitprovider/types.go index 46e40b7cb6..3381e95fd0 100644 --- a/pkg/views/gitprovider/types.go +++ b/pkg/views/gitprovider/types.go @@ -4,11 +4,13 @@ package gitprovider type GitProviderView struct { - Id string - ProviderId string - Name string - Username string - BaseApiUrl string - Token string - Alias string + Id string + ProviderId string + Name string + Username string + BaseApiUrl string + Token string + Alias string + SigningMethod string + SigningKey string }