diff --git a/cmd/bridge/main.go b/cmd/bridge/main.go index 1fd27be5a1b..8b6f8b041a5 100644 --- a/cmd/bridge/main.go +++ b/cmd/bridge/main.go @@ -128,6 +128,7 @@ func main() { _ = fs.String("kubectl-client-secret-file", "", "DEPRECATED: setting this does not do anything.") fK8sPublicEndpoint := fs.String("k8s-public-endpoint", "", "Endpoint to use to communicate to the API server.") + fCustomLoginServerURL := fs.String("custom-login-server-url", "", "Optional URL to display as the --server value in the 'oc login' command shown in the console. Does not affect API proxying. When unset the console uses the cluster API server URL.") fBranding := fs.String("branding", "okd", "Console branding for the masthead logo and title. One of okd, openshift, ocp, online, dedicated, azure, or rosa. Defaults to okd.") fCustomProductName := fs.String("custom-product-name", "", "Custom product name for console branding.") @@ -638,6 +639,9 @@ func main() { apiServerEndpoint = srv.K8sProxyConfig.Endpoint.String() } srv.KubeAPIServerURL = apiServerEndpoint + _, err = flags.ValidateFlagIsURL("custom-login-server-url", *fCustomLoginServerURL, true) + flags.FatalIfFailed(err) + srv.LoginServerURL = *fCustomLoginServerURL clusterManagementURL, err := url.Parse(clusterManagementURL) if err != nil { diff --git a/frontend/@types/console/window.d.ts b/frontend/@types/console/window.d.ts index 81e2fa6078c..ad3463870d0 100644 --- a/frontend/@types/console/window.d.ts +++ b/frontend/@types/console/window.d.ts @@ -15,6 +15,7 @@ declare interface Window { documentationBaseURL: string; kubeAdminLogoutURL: string; kubeAPIServerURL: string; + customLoginServerURL?: string; loadTestFactor: number; loginErrorURL: string; loginSuccessURL: string; diff --git a/frontend/packages/console-shared/src/hooks/useCopyLoginCommands.ts b/frontend/packages/console-shared/src/hooks/useCopyLoginCommands.ts index 6bd29cecd4a..0cab9bd8d28 100644 --- a/frontend/packages/console-shared/src/hooks/useCopyLoginCommands.ts +++ b/frontend/packages/console-shared/src/hooks/useCopyLoginCommands.ts @@ -5,9 +5,10 @@ import { useFlag } from './flag'; const COPY_LOGIN_COMMANDS_ENDPOINT = '/api/copy-login-commands'; -export const useCopyLoginCommands = (): [string, string] => { +export const useCopyLoginCommands = (): [string, string, string] => { const [requestTokenURL, setRequestTokenURL] = useState(); const [externalLoginCommand, setExternalLoginCommand] = useState(); + const [loginServerURL, setLoginServerURL] = useState(''); const authEnabled = useFlag(FLAGS.AUTH_ENABLED); useEffect(() => { if (authEnabled) { @@ -15,6 +16,8 @@ export const useCopyLoginCommands = (): [string, string] => { .then((resp) => { const newRequestTokenURL = resp?.requestTokenURL ?? ''; const newExternalLoginCommand = resp?.externalLoginCommand ?? ''; + const newLoginServerURL = resp?.customLoginServerURL ?? ''; + setLoginServerURL(newLoginServerURL); if (newRequestTokenURL) { setRequestTokenURL(newRequestTokenURL); setExternalLoginCommand(''); @@ -28,9 +31,10 @@ export const useCopyLoginCommands = (): [string, string] => { console.warn(`GET ${COPY_LOGIN_COMMANDS_ENDPOINT} failed: ${err}`); setRequestTokenURL(''); setExternalLoginCommand(''); + setLoginServerURL(''); }); } }, [authEnabled]); - return [requestTokenURL, externalLoginCommand]; + return [requestTokenURL, externalLoginCommand, loginServerURL]; }; diff --git a/pkg/server/server.go b/pkg/server/server.go index 728f0b4d6d0..54428a09e20 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -10,6 +10,7 @@ import ( "net/url" "os" "path" + "regexp" "strings" "time" @@ -122,6 +123,7 @@ type jsGlobals struct { InactivityTimeout int `json:"inactivityTimeout"` KubeAdminLogoutURL string `json:"kubeAdminLogoutURL"` KubeAPIServerURL string `json:"kubeAPIServerURL"` + CustomLoginServerURL string `json:"customLoginServerURL,omitempty"` K8sMode string `json:"k8sMode"` LoadTestFactor int `json:"loadTestFactor"` LoginErrorURL string `json:"loginErrorURL"` @@ -192,6 +194,7 @@ type Server struct { KnativeChannelCRDLister ResourceLister KnativeEventSourceCRDLister ResourceLister KubeAPIServerURL string // JS global only. Not used for proxying. + LoginServerURL string KubeVersion string LoadTestFactor int MonitoringDashboardConfigMapLister ResourceLister @@ -758,6 +761,7 @@ func (s *Server) indexHandler(w http.ResponseWriter, r *http.Request) { K8sMode: s.K8sMode, KubeAdminLogoutURL: s.Authenticator.GetSpecialURLs().KubeAdminLogout, KubeAPIServerURL: s.KubeAPIServerURL, + CustomLoginServerURL: s.LoginServerURL, LoadTestFactor: s.LoadTestFactor, LoginErrorURL: proxy.SingleJoiningSlash(s.BaseURL.String(), AuthLoginErrorEndpoint), LoginSuccessURL: proxy.SingleJoiningSlash(s.BaseURL.String(), AuthLoginSuccessEndpoint), @@ -829,6 +833,29 @@ func notFoundHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("not found")) } +// serverFlagRe matches the --server=VALUE token in an oc login command string. +var serverFlagRe = regexp.MustCompile(`(^|\s)--server=\S+`) + +// applyLoginServerURL substitutes (or appends) the --server= flag in an oc +// login command string with loginServerURL. Returns cmd unchanged when either +// argument is empty. +func applyLoginServerURL(cmd, loginServerURL string) string { + if loginServerURL == "" || cmd == "" { + return cmd + } + serverFlag := "--server=" + loginServerURL + if serverFlagRe.MatchString(cmd) { + return serverFlagRe.ReplaceAllStringFunc(cmd, func(match string) string { + // Preserve any leading whitespace captured by the group. + if strings.HasPrefix(match, " ") || strings.HasPrefix(match, "\t") { + return match[:1] + serverFlag + } + return serverFlag + }) + } + return cmd + " " + serverFlag +} + func (s *Server) handleCopyLogin(w http.ResponseWriter, r *http.Request) { if r.Method != "GET" { serverutils.SendResponse(w, http.StatusMethodNotAllowed, serverutils.ApiError{Err: "Invalid method: only GET is allowed"}) @@ -840,9 +867,14 @@ func (s *Server) handleCopyLogin(w http.ResponseWriter, r *http.Request) { serverutils.SendResponse(w, http.StatusOK, struct { RequestTokenURL string `json:"requestTokenURL"` ExternalLoginCommand string `json:"externalLoginCommand"` + // CustomLoginServerURL is included when the operator has configured an override + // server address (e.g. a Proxy). Empty string means the frontend + // should use the default cluster API server URL. + CustomLoginServerURL string `json:"customLoginServerURL,omitempty"` }{ RequestTokenURL: specialAuthURLs.RequestToken, - ExternalLoginCommand: s.Authenticator.GetOCLoginCommand(), + ExternalLoginCommand: applyLoginServerURL(s.Authenticator.GetOCLoginCommand(), s.LoginServerURL), + CustomLoginServerURL: s.LoginServerURL, }) } diff --git a/pkg/serverconfig/config.go b/pkg/serverconfig/config.go index c8cd6be0b80..6457347de48 100644 --- a/pkg/serverconfig/config.go +++ b/pkg/serverconfig/config.go @@ -412,6 +412,10 @@ func addCustomization(fs *flag.FlagSet, customization *Customization) { fs.Set("capabilities", string(capabilities)) } } + + if customization.CustomLoginServerURL != "" { + fs.Set("custom-login-server-url", customization.CustomLoginServerURL) + } } func isAlreadySet(fs *flag.FlagSet, name string) bool { diff --git a/pkg/serverconfig/types.go b/pkg/serverconfig/types.go index 6150bb82d0d..b495f2c0599 100644 --- a/pkg/serverconfig/types.go +++ b/pkg/serverconfig/types.go @@ -110,6 +110,11 @@ type Customization struct { DocumentationBaseURL string `yaml:"documentationBaseURL,omitempty"` CustomProductName string `yaml:"customProductName,omitempty"` CustomLogoFile string `yaml:"customLogoFile,omitempty"` + // CustomLoginServerURL, when set, overrides the API server URL shown in the + // 'oc login --server=...' command displayed by the console. The actual + // API proxy target is not affected. This is an opt-in field; when omitted + // the console falls back to the cluster API server URL. + CustomLoginServerURL string `yaml:"customLoginServerURL,omitempty"` // developerCatalog allows to configure the shown developer catalog categories and it's types. DeveloperCatalog DeveloperConsoleCatalogCustomization `yaml:"developerCatalog,omitempty"` QuickStarts QuickStarts `yaml:"quickStarts,omitempty"` diff --git a/vendor/github.com/openshift/api/operator/v1/types_console.go b/vendor/github.com/openshift/api/operator/v1/types_console.go index 35795b2b71b..5ab822a18d1 100644 --- a/vendor/github.com/openshift/api/operator/v1/types_console.go +++ b/vendor/github.com/openshift/api/operator/v1/types_console.go @@ -338,6 +338,20 @@ type ConsoleCustomization struct { // +listMapKey=id // +optional Perspectives []Perspective `json:"perspectives"` + // customLoginServerURL is an optional field that, when set, overrides the server + // address displayed in the 'oc login' command shown in the console. Use this + // to advertise an alternative API endpoint (for example, a Proxy + // or any other front-end that accepts oc login traffic) without changing + // how the console itself communicates with the Kubernetes API server. + // When omitted, the console falls back to the standard cluster API server URL. + // The value must be either empty or an absolute HTTPS URL (i.e. starting with + // 'https://') and must not exceed 1024 characters. + // +openshift:enable:FeatureGate=ConsoleCustomLoginServerURL + // +optional + // +kubebuilder:validation:XValidation:rule="size(self) == 0 || isURL(self)",message="custom login server url must be a valid absolute URL" + // +kubebuilder:validation:XValidation:rule="size(self) == 0 || url(self).getScheme() == 'https'",message="custom login server url scheme must be https" + // +kubebuilder:validation:MaxLength=1024 + CustomLoginServerURL string `json:"customLoginServerURL,omitempty"` } // ProjectAccess contains options for project access roles