diff --git a/api/v1/git_server_types.go b/api/v1/git_server_types.go index 979520e5..ffb49462 100644 --- a/api/v1/git_server_types.go +++ b/api/v1/git_server_types.go @@ -34,7 +34,7 @@ type GitServerSpec struct { // For Gerrit provider, only id_rsa key is required and used. // When id_rsa is present the operator connects over SSH and verifies the server's // host key against the operator's ssh-known-hosts ConfigMap; hosts other than - // github.com, gitlab.com and bitbucket.org must be added there first. + // github.com, gitlab.com, bitbucket.org and ssh.bitbucket.org must be added there first. // +kubebuilder:example:=my-git-credentials // +required NameSshKeySecret string `json:"nameSshKeySecret"` diff --git a/config/crd/bases/v2.edp.epam.com_gitservers.yaml b/config/crd/bases/v2.edp.epam.com_gitservers.yaml index aa985f18..1cd452e8 100644 --- a/config/crd/bases/v2.edp.epam.com_gitservers.yaml +++ b/config/crd/bases/v2.edp.epam.com_gitservers.yaml @@ -85,7 +85,7 @@ spec: For Gerrit provider, only id_rsa key is required and used. When id_rsa is present the operator connects over SSH and verifies the server's host key against the operator's ssh-known-hosts ConfigMap; hosts other than - github.com, gitlab.com and bitbucket.org must be added there first. + github.com, gitlab.com, bitbucket.org and ssh.bitbucket.org must be added there first. example: my-git-credentials type: string skipWebhookSSLVerification: diff --git a/controllers/gitserver/ssh.go b/controllers/gitserver/ssh.go index a9059d9a..b0b9b5c4 100644 --- a/controllers/gitserver/ssh.go +++ b/controllers/gitserver/ssh.go @@ -51,7 +51,7 @@ func checkGitServerConnection(data gitSshData, log logr.Logger) error { func extractSshData(gitServer *model.GitServer, secret *corev1.Secret) gitSshData { return gitSshData{ - Host: gitServer.GitHost, + Host: util.SSHHost(gitServer.GitProvider, gitServer.GitHost), User: gitServer.GitUser, Key: string(secret.Data[util.PrivateSShKeyName]), Port: gitServer.SshPort, diff --git a/controllers/gitserver/ssh_test.go b/controllers/gitserver/ssh_test.go index 3d8b169e..5bb1c335 100644 --- a/controllers/gitserver/ssh_test.go +++ b/controllers/gitserver/ssh_test.go @@ -8,6 +8,11 @@ import ( "github.com/go-logr/logr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + + codebaseApi "github.com/epam/edp-codebase-operator/v2/api/v1" + "github.com/epam/edp-codebase-operator/v2/pkg/model" + "github.com/epam/edp-codebase-operator/v2/pkg/util" ) func Test_sshInitFromSecret_verifiesHostKeys(t *testing.T) { @@ -32,6 +37,21 @@ func Test_sshInitFromSecret_verifiesHostKeys(t *testing.T) { assert.Equal(t, []string{"ssh-ed25519"}, client.Config.HostKeyAlgorithms) } +func Test_extractSshData_bitbucketCloudUsesSSHHost(t *testing.T) { + t.Parallel() + + data := extractSshData(&model.GitServer{ + GitHost: "bitbucket.org", + GitProvider: codebaseApi.GitProviderBitbucket, + GitUser: "git", + SshPort: 22, + }, &corev1.Secret{Data: map[string][]byte{util.PrivateSShKeyName: []byte("key")}}) + + assert.Equal(t, "ssh.bitbucket.org", data.Host) + assert.Equal(t, int32(22), data.Port) + assert.Equal(t, "key", data.Key) +} + func Test_sshInitFromSecret_failsWithoutKnownHosts(t *testing.T) { t.Setenv("SSH_KNOWN_HOSTS", filepath.Join(t.TempDir(), "absent")) diff --git a/deploy-templates/README.md b/deploy-templates/README.md index 1ffd7452..fb0d4829 100644 --- a/deploy-templates/README.md +++ b/deploy-templates/README.md @@ -47,7 +47,7 @@ A Helm chart for KubeRocketCI Codebase Operator | jira.quickLink | object | `{"enabled":true}` | Enable creation of QuickLink for Jira | | jira.rootUrl | string | `"https://jiraeu.example.com"` | URL to Jira server | | knownHosts.entries | string | `""` (no self-hosted servers pinned) | Host keys for self-hosted git servers, in known_hosts format, one per line. Obtain them with `ssh-keyscan -t rsa,ecdsa,ed25519 -p ` and verify the fingerprints out-of-band before trusting them. Servers on a port other than 22 must use the bracket form, e.g. `[git.example.com]:2222 ssh-ed25519 AAAA...`. | -| knownHosts.includeDefaultProviders | bool | `true` | Include the shipped host keys for github.com, gitlab.com and bitbucket.org. Disable only if you pin these hosts yourself through `entries`. | +| knownHosts.includeDefaultProviders | bool | `true` | Include the shipped host keys for github.com, gitlab.com, bitbucket.org and ssh.bitbucket.org. Disable only if you pin these hosts yourself through `entries`. | | name | string | `"codebase-operator"` | component name | | nodeSelector | object | `{}` | | | podLabels | object | `{}` | Labels to be added to the pod | diff --git a/deploy-templates/crds/v2.edp.epam.com_gitservers.yaml b/deploy-templates/crds/v2.edp.epam.com_gitservers.yaml index aa985f18..1cd452e8 100644 --- a/deploy-templates/crds/v2.edp.epam.com_gitservers.yaml +++ b/deploy-templates/crds/v2.edp.epam.com_gitservers.yaml @@ -85,7 +85,7 @@ spec: For Gerrit provider, only id_rsa key is required and used. When id_rsa is present the operator connects over SSH and verifies the server's host key against the operator's ssh-known-hosts ConfigMap; hosts other than - github.com, gitlab.com and bitbucket.org must be added there first. + github.com, gitlab.com, bitbucket.org and ssh.bitbucket.org must be added there first. example: my-git-credentials type: string skipWebhookSSLVerification: diff --git a/deploy-templates/templates/ssh-known-hosts-cm.yaml b/deploy-templates/templates/ssh-known-hosts-cm.yaml index a1491ea6..e271ab6a 100644 --- a/deploy-templates/templates/ssh-known-hosts-cm.yaml +++ b/deploy-templates/templates/ssh-known-hosts-cm.yaml @@ -27,6 +27,9 @@ data: bitbucket.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPIQmuzMBuKdWeF4+a2sjSSpBK0iqitSQ+5BM9KhpexuGt20JpTVM7u5BDZngncgrqDMbWdxMWWOGtZ9UgbqgZE= bitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M= + ssh.bitbucket.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPIQmuzMBuKdWeF4+a2sjSSpBK0iqitSQ+5BM9KhpexuGt20JpTVM7u5BDZngncgrqDMbWdxMWWOGtZ9UgbqgZE= + ssh.bitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO + ssh.bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M= {{- end }} {{- with .Values.knownHosts.entries }} {{- tpl . $ | nindent 4 }} diff --git a/deploy-templates/values.yaml b/deploy-templates/values.yaml index 2e9fc015..f98a50ca 100644 --- a/deploy-templates/values.yaml +++ b/deploy-templates/values.yaml @@ -93,7 +93,7 @@ branchStaleCheckInterval: 24h # against these entries; there is no way to disable verification. GitServers that # authenticate with a token over HTTPS are unaffected. knownHosts: - # -- Include the shipped host keys for github.com, gitlab.com and bitbucket.org. + # -- Include the shipped host keys for github.com, gitlab.com, bitbucket.org and ssh.bitbucket.org. # Disable only if you pin these hosts yourself through `entries`. includeDefaultProviders: true # -- Host keys for self-hosted git servers, in known_hosts format, one per line. diff --git a/docs/api.md b/docs/api.md index c2487f1c..92fefb5c 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1349,7 +1349,7 @@ Optional keys: For Gerrit provider, only id_rsa key is required and used. When id_rsa is present the operator connects over SSH and verifies the server's host key against the operator's ssh-known-hosts ConfigMap; hosts other than -github.com, gitlab.com and bitbucket.org must be added there first.
+github.com, gitlab.com, bitbucket.org and ssh.bitbucket.org must be added there first.
true diff --git a/docs/ssh-known-hosts.md b/docs/ssh-known-hosts.md index 64218ec6..1df2e85e 100644 --- a/docs/ssh-known-hosts.md +++ b/docs/ssh-known-hosts.md @@ -19,9 +19,15 @@ it into the operator at `/etc/codebase-operator/ssh`. The operator reads `/etc/codebase-operator/ssh/ssh_known_hosts`, via the `SSH_KNOWN_HOSTS` environment variable set by the chart. -The ConfigMap ships with host keys for `github.com`, `gitlab.com` and -`bitbucket.org`, so SSH GitServers pointing at those providers work with no -further configuration. +The ConfigMap ships with host keys for `github.com`, `gitlab.com`, +`bitbucket.org` and `ssh.bitbucket.org`, so SSH GitServers pointing at those +providers work with no further configuration. + +Bitbucket Cloud moves SSH to `ssh.bitbucket.org`; SSH to `bitbucket.org` is +refused after 2026-12-10. Keep `gitHost: bitbucket.org` on the GitServer: the +operator uses `ssh.bitbucket.org` for SSH and `bitbucket.org` for HTTPS and +links. With `knownHosts.includeDefaultProviders: false`, add the +`ssh.bitbucket.org` keys to `knownHosts.entries`. The ConfigMap is mounted as a directory rather than with `subPath`, so the kubelet refreshes it in place and the operator re-reads it on the next @@ -124,7 +130,7 @@ kubectl get gitservers -o json | jq -r '.items[] | select(.spec.gitProvider) | ``` For each one whose secret contains an `id_rsa` key and whose host is not -github.com, gitlab.com or bitbucket.org, add its host keys to +github.com, gitlab.com, bitbucket.org or ssh.bitbucket.org, add its host keys to `knownHosts.entries` as part of the upgrade. GitServers left unpinned will report `connected: false` until their keys are added; no data is lost and the operator recovers on the next reconcile once the entry is present. diff --git a/pkg/model/git-server.go b/pkg/model/git-server.go index 273ff102..69b56f1c 100644 --- a/pkg/model/git-server.go +++ b/pkg/model/git-server.go @@ -23,6 +23,7 @@ type ActionLog struct { type GitServer struct { GitHost string + GitProvider string GitUser string HttpsPort int32 SshPort int32 @@ -47,6 +48,7 @@ func ConvertToGitServer(k8sObj *codebaseApi.GitServer) *GitServer { return &GitServer{ GitHost: spec.GitHost, + GitProvider: spec.GitProvider, GitUser: spec.GitUser, HttpsPort: spec.HttpsPort, SshPort: spec.SshPort, diff --git a/pkg/model/git-server_test.go b/pkg/model/git-server_test.go index ca71dcdf..0317a65d 100644 --- a/pkg/model/git-server_test.go +++ b/pkg/model/git-server_test.go @@ -13,9 +13,11 @@ func TestConvertToGitServer(t *testing.T) { gs := ConvertToGitServer(&codebaseApi.GitServer{ Spec: codebaseApi.GitServerSpec{ - GitHost: "github.com", + GitHost: "github.com", + GitProvider: codebaseApi.GitProviderGithub, }, }) - assert.Equal(t, gs.GitHost, "github.com") + assert.Equal(t, "github.com", gs.GitHost) + assert.Equal(t, codebaseApi.GitProviderGithub, gs.GitProvider) } diff --git a/pkg/util/consts.go b/pkg/util/consts.go index 6839a917..c3951cdc 100644 --- a/pkg/util/consts.go +++ b/pkg/util/consts.go @@ -50,6 +50,9 @@ const ( GithubDomain = "https://github.com/epmd-edp" + BitbucketCloudHost = "bitbucket.org" + BitbucketCloudSSHHost = "ssh.bitbucket.org" + CITekton = "tekton" CIGitLab = "gitlab" diff --git a/pkg/util/url.go b/pkg/util/url.go index f34caafa..2ffb59ff 100644 --- a/pkg/util/url.go +++ b/pkg/util/url.go @@ -83,13 +83,25 @@ func GetHostWithProtocol(host string) string { return fmt.Sprintf("https://%v", host) } +// SSHHost returns the host for SSH connections to a git server. +// Bitbucket Cloud SSH endpoint: ssh.bitbucket.org. HTTPS and web links stay on gitHost. +func SSHHost(gitProvider, gitHost string) string { + if gitProvider == codebaseApi.GitProviderBitbucket && strings.EqualFold(gitHost, BitbucketCloudHost) { + return BitbucketCloudSSHHost + } + + return gitHost +} + // GetSSHUrl returns ssh url for git server. func GetSSHUrl(gitServer *codebaseApi.GitServer, repoName string) string { + host := SSHHost(gitServer.Spec.GitProvider, gitServer.Spec.GitHost) + if gitServer.Spec.GitProvider == codebaseApi.GitProviderGerrit { - return fmt.Sprintf("ssh://%s:%d/%s", gitServer.Spec.GitHost, gitServer.Spec.SshPort, repoName) + return fmt.Sprintf("ssh://%s:%d/%s", host, gitServer.Spec.SshPort, repoName) } - return fmt.Sprintf("git@%s:%s.git", gitServer.Spec.GitHost, repoName) + return fmt.Sprintf("git@%s:%s.git", host, repoName) } // GetHTTPSUrl returns https url for git server and codebase. diff --git a/pkg/util/url_test.go b/pkg/util/url_test.go index d185d92d..d09ce9c9 100644 --- a/pkg/util/url_test.go +++ b/pkg/util/url_test.go @@ -277,6 +277,18 @@ func TestGetSSHUrl(t *testing.T) { repoName: "owner/repo", want: "git@github.com:owner/repo.git", }, + { + name: "should use ssh.bitbucket.org for bitbucket cloud", + gitServer: &codebaseApi.GitServer{ + Spec: codebaseApi.GitServerSpec{ + GitHost: "bitbucket.org", + GitProvider: codebaseApi.GitProviderBitbucket, + SshPort: 22, + }, + }, + repoName: "workspace/repo", + want: "git@ssh.bitbucket.org:workspace/repo.git", + }, } for _, tt := range tests { @@ -288,6 +300,56 @@ func TestGetSSHUrl(t *testing.T) { } } +func TestSSHHost(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + gitProvider string + gitHost string + want string + }{ + { + name: "bitbucket cloud maps to ssh host", + gitProvider: codebaseApi.GitProviderBitbucket, + gitHost: "bitbucket.org", + want: "ssh.bitbucket.org", + }, + { + name: "bitbucket cloud host is case-insensitive", + gitProvider: codebaseApi.GitProviderBitbucket, + gitHost: "BitBucket.org", + want: "ssh.bitbucket.org", + }, + { + name: "ssh.bitbucket.org is kept", + gitProvider: codebaseApi.GitProviderBitbucket, + gitHost: "ssh.bitbucket.org", + want: "ssh.bitbucket.org", + }, + { + name: "self-hosted bitbucket is kept", + gitProvider: codebaseApi.GitProviderBitbucket, + gitHost: "bitbucket.example.com", + want: "bitbucket.example.com", + }, + { + name: "other provider with bitbucket.org host is kept", + gitProvider: codebaseApi.GitProviderGithub, + gitHost: "bitbucket.org", + want: "bitbucket.org", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, tt.want, SSHHost(tt.gitProvider, tt.gitHost)) + }) + } +} + func TestGetProjectGitUrl(t *testing.T) { tests := []struct { name string