From ce270f2b2564a63172cc2b5693de4d2dd5eed55a Mon Sep 17 00:00:00 2001 From: Christopher Collins Date: Sun, 5 Apr 2026 12:36:02 -1000 Subject: [PATCH] Fix Jira integration for Atlassian Cloud migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Red Hat migrated Jira from on-prem (issues.redhat.com) to Atlassian Cloud (redhat.atlassian.net). This broke access request creation because: 1. The default Jira base URL pointed to the old instance 2. PAT authentication is not supported by Atlassian Cloud - it requires Basic Auth (email + API token) 3. URL parsing in ohssService hardcoded ".com" domain suffix Changes: - Update default JiraBaseURL to redhat.atlassian.net - Switch from PATAuthTransport to BasicAuthTransport (email + API token) - Add JiraEmail config field with JIRA_EMAIL env var support - Fix URL parsing in ohssService to use net/url instead of string split - Add tests for URL parsing, ClusterID extraction, and JiraEmail config Mirrors the approach taken in osdctl commit 54dbc7aefe1f. Created with assistance from Claude 🤖 Signed-off-by: Christopher Collins --- pkg/cli/config/config.go | 11 ++++- pkg/cli/config/config_test.go | 80 +++++++++++++++++++++++++++++++++++ pkg/info/info.go | 1 + pkg/jira/issueService.go | 11 +++-- pkg/jira/ohssService.go | 7 +-- pkg/jira/ohssService_test.go | 42 ++++++++++++++++++ 6 files changed, 145 insertions(+), 7 deletions(-) diff --git a/pkg/cli/config/config.go b/pkg/cli/config/config.go index a006ce14..07ddb31f 100644 --- a/pkg/cli/config/config.go +++ b/pkg/cli/config/config.go @@ -44,6 +44,7 @@ type BackplaneConfiguration struct { PagerDutyAPIKey string `json:"pd-key"` JiraBaseURL string `json:"jira-base-url"` JiraToken string `json:"jira-token"` + JiraEmail string `json:"jira-email"` JiraConfigForAccessRequests AccessRequestsJiraConfiguration `json:"jira-config-for-access-requests"` VPNCheckEndpoint string `json:"vpn-check-endpoint"` ProxyCheckEndpoint string `json:"proxy-check-endpoint"` @@ -58,9 +59,10 @@ const ( JiraBaseURLKey = "jira-base-url" AssumeInitialArnKey = "assume-initial-arn" JiraTokenViperKey = "jira-token" + JiraEmailViperKey = "jira-email" JiraConfigForAccessRequestsKey = "jira-config-for-access-requests" prodEnvNameDefaultValue = "production" - JiraBaseURLDefaultValue = "https://issues.redhat.com" + JiraBaseURLDefaultValue = "https://redhat.atlassian.net" proxyTestTimeout = 10 * time.Second GovcloudDefaultValue bool = false GovcloudDefaultValueKey = "govcloud" @@ -232,6 +234,13 @@ func GetBackplaneConfigurationWithConn(ocmConn *ocmsdk.Connection) (bpConfig Bac bpConfig.JiraToken = viper.GetString(JiraTokenViperKey) } + // JIRA email is required for Atlassian Cloud basic auth + // JIRA_EMAIL env var takes precedence, fallback to config file + bpConfig.JiraEmail = os.Getenv(info.BackplaneJiraEmailEnvName) + if bpConfig.JiraEmail == "" { + bpConfig.JiraEmail = viper.GetString(JiraEmailViperKey) + } + // JIRA config for access requests is optional as there is a default value err = viper.UnmarshalKey(JiraConfigForAccessRequestsKey, &bpConfig.JiraConfigForAccessRequests) diff --git a/pkg/cli/config/config_test.go b/pkg/cli/config/config_test.go index 58d31888..e6b19252 100644 --- a/pkg/cli/config/config_test.go +++ b/pkg/cli/config/config_test.go @@ -75,6 +75,86 @@ func TestGetBackplaneConfig(t *testing.T) { } }) + t.Run("it reads JIRA email from JIRA_EMAIL environment variable when config file is empty", func(t *testing.T) { + viper.Reset() + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("dummy data")) + })) + + expectedEmail := "user@redhat.com" + userDefinedProxy := "example-proxy" + t.Setenv("BACKPLANE_URL", svr.URL) + t.Setenv("HTTPS_PROXY", userDefinedProxy) + t.Setenv("JIRA_EMAIL", expectedEmail) + + config, err := GetBackplaneConfiguration() + if err != nil { + t.Error(err) + } + + if config.JiraEmail != expectedEmail { + t.Errorf("expected JiraEmail to be %s, got %s", expectedEmail, config.JiraEmail) + } + }) + + t.Run("JIRA_EMAIL environment variable takes precedence over config file value", func(t *testing.T) { + viper.Reset() + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("dummy data")) + })) + + configEmail := "config@redhat.com" + envEmail := "env@redhat.com" + + t.Setenv("BACKPLANE_URL", svr.URL) + t.Setenv("JIRA_EMAIL", envEmail) + + // Simulate config file value + viper.Set(JiraEmailViperKey, configEmail) + + config, err := GetBackplaneConfiguration() + if err != nil { + t.Error(err) + } + + if config.JiraEmail != envEmail { + t.Errorf("expected environment variable email to take precedence: expected %s, got %s", envEmail, config.JiraEmail) + } + }) + + t.Run("JiraEmail falls back to config file when JIRA_EMAIL env var is not set", func(t *testing.T) { + viper.Reset() + + tmpDir := t.TempDir() + configPath := tmpDir + "/config.json" + configContent := `{ + "jira-email": "configfile@redhat.com" + }` + err := os.WriteFile(configPath, []byte(configContent), 0600) + if err != nil { + t.Fatal(err) + } + + t.Setenv("BACKPLANE_CONFIG", configPath) + + svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("dummy data")) + })) + defer svr.Close() + + t.Setenv("BACKPLANE_URL", svr.URL) + t.Setenv("HTTPS_PROXY", "example-proxy") + + config, err := GetBackplaneConfiguration() + if err != nil { + t.Error(err) + } + + if config.JiraEmail != "configfile@redhat.com" { + t.Errorf("expected JiraEmail from config file, got %s", config.JiraEmail) + } + }) + t.Run("JIRA_API_TOKEN environment variable takes precedence over config file JIRA token", func(t *testing.T) { viper.Reset() svr := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/info/info.go b/pkg/info/info.go index 79570a4e..a75b4a95 100644 --- a/pkg/info/info.go +++ b/pkg/info/info.go @@ -15,6 +15,7 @@ const ( BackplaneConfigPathEnvName = "BACKPLANE_CONFIG" BackplaneKubeconfigEnvName = "KUBECONFIG" BackplaneJiraAPITokenEnvName = "JIRA_API_TOKEN" //nolint:gosec + BackplaneJiraEmailEnvName = "JIRA_EMAIL" // Configuration BackplaneConfigDefaultFilePath = ".config/backplane" diff --git a/pkg/jira/issueService.go b/pkg/jira/issueService.go index 901935f4..e96e3269 100644 --- a/pkg/jira/issueService.go +++ b/pkg/jira/issueService.go @@ -86,11 +86,16 @@ func createIssueService() (*jira.IssueService, error) { } if bpConfig.JiraToken == "" { - return nil, fmt.Errorf("JIRA token is not defined, consider defining it running 'ocm-backplane config set %s '", config.JiraTokenViperKey) + return nil, fmt.Errorf("JIRA API token is not defined, consider defining it running 'ocm-backplane config set %s '", config.JiraTokenViperKey) } - transport := jira.PATAuthTransport{ - Token: bpConfig.JiraToken, + if bpConfig.JiraEmail == "" { + return nil, fmt.Errorf("JIRA email is not defined, consider defining it running 'ocm-backplane config set %s ' or setting the JIRA_EMAIL environment variable", config.JiraEmailViperKey) + } + + transport := jira.BasicAuthTransport{ + Username: bpConfig.JiraEmail, + Password: bpConfig.JiraToken, } jiraClient, err := jira.NewClient(transport.Client(), bpConfig.JiraBaseURL) diff --git a/pkg/jira/ohssService.go b/pkg/jira/ohssService.go index be76ed4d..77ae2ff4 100644 --- a/pkg/jira/ohssService.go +++ b/pkg/jira/ohssService.go @@ -2,7 +2,7 @@ package jira import ( "fmt" - "strings" + "net/url" "github.com/andygrunwald/go-jira" ) @@ -72,8 +72,9 @@ func (j *OHSSService) formatIssue(issue jira.Issue) (formatIssue OHSSIssue, err formatIssue.Title = issue.Fields.Summary } if issue.Self != "" { - selfSlice := strings.SplitAfter(issue.Self, ".com") - formatIssue.WebURL = fmt.Sprintf("%s/browse/%s", selfSlice[0], issue.Key) + if u, err := url.Parse(issue.Self); err == nil { + formatIssue.WebURL = fmt.Sprintf("%s://%s/browse/%s", u.Scheme, u.Host, issue.Key) + } } return formatIssue, nil diff --git a/pkg/jira/ohssService_test.go b/pkg/jira/ohssService_test.go index f334343c..ab17d89f 100644 --- a/pkg/jira/ohssService_test.go +++ b/pkg/jira/ohssService_test.go @@ -62,5 +62,47 @@ var _ = Describe("Jira", func() { Expect(err).NotTo(BeNil()) Expect(err.Error()).To(Equal("no matching issue for issueID:OHSS-1000")) }) + + It("Should parse Atlassian Cloud .net Self URL into WebURL", func() { + testIssue.Key = testOHSSID + testIssue.Self = "https://redhat.atlassian.net/rest/api/2/issue/12345" + mockIssueService.EXPECT().Get(testOHSSID, nil).Return(&testIssue, nil, nil).Times(1) + + issue, err := ohssService.GetIssue(testOHSSID) + Expect(err).To(BeNil()) + Expect(issue.WebURL).To(Equal("https://redhat.atlassian.net/browse/OHSS-1000")) + }) + + It("Should parse legacy .com Self URL into WebURL", func() { + testIssue.Key = testOHSSID + testIssue.Self = "https://issues.redhat.com/rest/api/2/issue/12345" + mockIssueService.EXPECT().Get(testOHSSID, nil).Return(&testIssue, nil, nil).Times(1) + + issue, err := ohssService.GetIssue(testOHSSID) + Expect(err).To(BeNil()) + Expect(issue.WebURL).To(Equal("https://issues.redhat.com/browse/OHSS-1000")) + }) + + It("Should return empty WebURL when Self is empty", func() { + testIssue.Key = testOHSSID + testIssue.Self = "" + mockIssueService.EXPECT().Get(testOHSSID, nil).Return(&testIssue, nil, nil).Times(1) + + issue, err := ohssService.GetIssue(testOHSSID) + Expect(err).To(BeNil()) + Expect(issue.WebURL).To(Equal("")) + }) + + It("Should extract ClusterID from custom field", func() { + issueFields.Unknowns = map[string]interface{}{ + CustomFieldClusterID: "abc-123-def-456", + } + testIssue.Fields = issueFields + mockIssueService.EXPECT().Get(testOHSSID, nil).Return(&testIssue, nil, nil).Times(1) + + issue, err := ohssService.GetIssue(testOHSSID) + Expect(err).To(BeNil()) + Expect(issue.ClusterID).To(Equal("abc-123-def-456")) + }) }) })