Skip to content

Commit

Permalink
Merge pull request #19194 from simo5/webconredir
Browse files Browse the repository at this point in the history
Dynamic discovery of Web Console Public URL
  • Loading branch information
openshift-merge-robot committed Apr 11, 2018
2 parents e27e09d + 0931eae commit 35f2751
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 55 deletions.
58 changes: 9 additions & 49 deletions pkg/cmd/server/origin/master.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"strings"
"time"
Expand All @@ -25,7 +24,6 @@ import (
routeplugin "github.com/openshift/origin/pkg/route/allocation/simple"
routeallocationcontroller "github.com/openshift/origin/pkg/route/controller/allocation"
sccstorage "github.com/openshift/origin/pkg/security/registry/securitycontextconstraints/etcd"
"github.com/openshift/origin/pkg/util/httprequest"
kapiserveroptions "k8s.io/kubernetes/cmd/kube-apiserver/app/options"
)

Expand Down Expand Up @@ -217,7 +215,7 @@ func (c *MasterConfig) Run(stopCh <-chan struct{}) error {
var delegateAPIServer apiserver.DelegationTarget
var extraPostStartHooks map[string]apiserver.PostStartHookFunc

c.kubeAPIServerConfig.GenericConfig.BuildHandlerChainFunc, extraPostStartHooks, err = c.buildHandlerChain(c.kubeAPIServerConfig.GenericConfig)
c.kubeAPIServerConfig.GenericConfig.BuildHandlerChainFunc, extraPostStartHooks, err = c.buildHandlerChain(c.kubeAPIServerConfig.GenericConfig, stopCh)
if err != nil {
return err
}
Expand Down Expand Up @@ -288,7 +286,7 @@ func (c *MasterConfig) RunKubeAPIServer(stopCh <-chan struct{}) error {
var delegateAPIServer apiserver.DelegationTarget
var extraPostStartHooks map[string]apiserver.PostStartHookFunc

c.kubeAPIServerConfig.GenericConfig.BuildHandlerChainFunc, extraPostStartHooks, err = c.buildHandlerChain(c.kubeAPIServerConfig.GenericConfig)
c.kubeAPIServerConfig.GenericConfig.BuildHandlerChainFunc, extraPostStartHooks, err = c.buildHandlerChain(c.kubeAPIServerConfig.GenericConfig, stopCh)
if err != nil {
return err
}
Expand Down Expand Up @@ -376,11 +374,7 @@ func (c *MasterConfig) RunOpenShift(stopCh <-chan struct{}) error {
return cmdutil.WaitForSuccessfulDial(true, c.Options.ServingInfo.BindNetwork, c.Options.ServingInfo.BindAddress, 100*time.Millisecond, 100*time.Millisecond, 100)
}

func (c *MasterConfig) buildHandlerChain(genericConfig *apiserver.Config) (func(apiHandler http.Handler, kc *apiserver.Config) http.Handler, map[string]apiserver.PostStartHookFunc, error) {
webconsolePublicURL := ""
if c.Options.OAuthConfig != nil {
webconsolePublicURL = c.Options.OAuthConfig.AssetPublicURL
}
func (c *MasterConfig) buildHandlerChain(genericConfig *apiserver.Config, stopCh <-chan struct{}) (func(apiHandler http.Handler, kc *apiserver.Config) http.Handler, map[string]apiserver.PostStartHookFunc, error) {
webconsoleProxyHandler, err := c.newWebConsoleProxy()
if err != nil {
return nil, nil, err
Expand All @@ -391,6 +385,10 @@ func (c *MasterConfig) buildHandlerChain(genericConfig *apiserver.Config) (func(
}

return func(apiHandler http.Handler, genericConfig *apiserver.Config) http.Handler {
// Machinery that let's use discover the Web Console Public URL
accessor := newWebConsolePublicURLAccessor(c.PrivilegedLoopbackClientConfig)
go accessor.Run(stopCh)

// these are after the kube handler
handler := c.versionSkewFilter(apiHandler, genericConfig.RequestContextMapper)

Expand All @@ -402,11 +400,11 @@ func (c *MasterConfig) buildHandlerChain(genericConfig *apiserver.Config) (func(
handler = withCacheControl(handler, "no-store") // protected endpoints should not be cached

// redirects from / to /console if you're using a browser
handler = withAssetServerRedirect(handler, webconsolePublicURL)
handler = withAssetServerRedirect(handler, accessor)

// these handlers are actually separate API servers which have their own handler chains.
// our server embeds these
handler = c.withConsoleRedirection(handler, webconsoleProxyHandler, webconsolePublicURL)
handler = c.withConsoleRedirection(handler, webconsoleProxyHandler, accessor)
handler = c.withOAuthRedirection(handler, oauthServerHandler)

return handler
Expand All @@ -424,44 +422,6 @@ func openshiftHandlerChain(apiHandler http.Handler, genericConfig *apiserver.Con
return handler
}

// If we know the location of the asset server, redirect to it when / is requested
// and the Accept header supports text/html
func withAssetServerRedirect(handler http.Handler, webconsolePublicURL string) http.Handler {
if len(webconsolePublicURL) == 0 {
return handler
}

return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/" {
if httprequest.PrefersHTML(req) {
http.Redirect(w, req, webconsolePublicURL, http.StatusFound)
}
}
// Dispatch to the next handler
handler.ServeHTTP(w, req)
})
}

func (c *MasterConfig) withConsoleRedirection(handler, assetServerHandler http.Handler, webconsolePublicURL string) http.Handler {
if len(webconsolePublicURL) == 0 {
return handler
}

publicURL, err := url.Parse(webconsolePublicURL)
if err != nil {
// fails validation before here
glog.Fatal(err)
}
// path always ends in a slash or the
prefix := publicURL.Path
lastIndex := len(publicURL.Path) - 1
if publicURL.Path[lastIndex] == '/' {
prefix = publicURL.Path[0:lastIndex]
}

return WithPatternPrefixHandler(handler, assetServerHandler, prefix)
}

func (c *MasterConfig) withOAuthRedirection(handler, oauthServerHandler http.Handler) http.Handler {
if c.Options.OAuthConfig == nil {
return handler
Expand Down
164 changes: 164 additions & 0 deletions pkg/cmd/server/origin/webconsole_proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package origin

import (
"fmt"
"net/http"
"net/url"
"strings"
"sync/atomic"
"time"

"github.com/golang/glog"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
coreclientv1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"

webconsoleconfigv1 "github.com/openshift/api/webconsole/v1"
"github.com/openshift/origin/pkg/util/httprequest"
)

// If we know the location of the asset server, redirect to it when / is requested
// and the Accept header supports text/html
func withAssetServerRedirect(handler http.Handler, accessor *webConsolePublicURLAccessor) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if req.URL.Path == "/" && httprequest.PrefersHTML(req) {
webconsolePublicURL := accessor.getPublicConsoleURL()
if len(webconsolePublicURL) > 0 {
http.Redirect(w, req, webconsolePublicURL, http.StatusFound)
return
}
w.Header().Set("Retry-After", "3")
w.WriteHeader(http.StatusServiceUnavailable)
return
}
// Dispatch to the next handler
handler.ServeHTTP(w, req)
})
}

func (c *MasterConfig) withConsoleRedirection(handler, assetServerHandler http.Handler, accessor *webConsolePublicURLAccessor) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
// blacklist well known paths so we do not risk recursion deadlocks
for _, prefix := range []string{"/apis", "/api", "/oapi", "/healtz", "/version"} {
if req.URL.Path == prefix || strings.HasPrefix(req.URL.Path, prefix+"/") {
// Dispatch to the next handler
handler.ServeHTTP(w, req)
return
}
}

webconsolePublicURL := accessor.getPublicConsoleURL()
if len(webconsolePublicURL) > 0 {
publicURL, err := url.Parse(webconsolePublicURL)
if err != nil {
// fails validation before here
glog.Fatal(err)
// Dispatch to the next handler
handler.ServeHTTP(w, req)
return
}

prefix := publicURL.Path
// prefix must not include a trailing '/'
lastIndex := len(publicURL.Path) - 1
if publicURL.Path[lastIndex] == '/' {
prefix = publicURL.Path[0:lastIndex]
}
if req.URL.Path == prefix || strings.HasPrefix(req.URL.Path, prefix+"/") {
assetServerHandler.ServeHTTP(w, req)
return
}
}

// Dispatch to the next handler
handler.ServeHTTP(w, req)
})
}

type webConsolePublicURLAccessor struct {
publicURL atomic.Value
configMapGetter coreclientv1.ConfigMapsGetter
polling time.Duration
}

func newWebConsolePublicURLAccessor(clientConfig rest.Config) *webConsolePublicURLAccessor {
accessor := &webConsolePublicURLAccessor{
configMapGetter: coreclientv1.NewForConfigOrDie(&clientConfig),
}
return accessor
}

func (a *webConsolePublicURLAccessor) getPublicConsoleURL() string {
currValue, ok := a.publicURL.Load().(string)
if ok && len(currValue) > 0 {
return currValue
}

// if we aren't already set, try to update
return a.updatePublicConsoleURL()
}

func (a *webConsolePublicURLAccessor) updatePublicConsoleURL() string {
// TODO: best effort ratelimit maybe
configMap, err := a.configMapGetter.ConfigMaps("openshift-web-console").Get("webconsole-config", metav1.GetOptions{})
if err != nil {
return ""
}
config, ok := configMap.Data["webconsole-config.yaml"]
if !ok {
return ""
}
webConsoleConfig, err := readWebConsoleConfiguration(config)
if err != nil {
return ""
}
newValue := webConsoleConfig.ClusterInfo.ConsolePublicURL
a.publicURL.Store(newValue)

// Once we have a value we relax polling to once per minute
a.polling = 1 * time.Minute

return newValue
}

func (a *webConsolePublicURLAccessor) Run(stopCh <-chan struct{}) {
defer utilruntime.HandleCrash()
a.polling = 1 * time.Second
for {
select {
case <-stopCh:
return
case <-time.After(a.polling):
}

a.updatePublicConsoleURL()
}
}

var (
webconsoleConfigScheme = runtime.NewScheme()
webconsoleConfigCodecs = serializer.NewCodecFactory(webconsoleConfigScheme)
)

func init() {
if err := webconsoleconfigv1.AddToScheme(webconsoleConfigScheme); err != nil {
panic(err)
}
}

func readWebConsoleConfiguration(objBytes string) (*webconsoleconfigv1.WebConsoleConfiguration, error) {
defaultConfigObj, err := runtime.Decode(webconsoleConfigCodecs.UniversalDecoder(webconsoleconfigv1.SchemeGroupVersion), []byte(objBytes))
if err != nil {
return nil, err
}
ret, ok := defaultConfigObj.(*webconsoleconfigv1.WebConsoleConfiguration)
if !ok {
return nil, fmt.Errorf("expected *webconsoleconfigv1.WebConsoleConfiguration, got %T", defaultConfigObj)
}

return ret, nil
}
79 changes: 73 additions & 6 deletions test/integration/master_routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,22 @@ import (
"net/http"
"reflect"
"sort"
"strconv"
"strings"
"testing"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/diff"
knet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/kubernetes/pkg/api/legacyscheme"
kapi "k8s.io/kubernetes/pkg/apis/core"

buildv1 "github.com/openshift/api/build/v1"
webconsoleconfigv1 "github.com/openshift/api/webconsole/v1"
build "github.com/openshift/origin/pkg/build/apis/build"
buildclient "github.com/openshift/origin/pkg/build/generated/internalclientset"
testutil "github.com/openshift/origin/test/util"
Expand Down Expand Up @@ -185,17 +190,79 @@ func TestRootRedirect(t *testing.T) {
t.Fatalf("Unexpected index: \ngot=%v,\n\n expected=%v,\n\ndiff=%v", got.Paths, expectedIndex, diff.ObjectDiff(expectedIndex, got.Paths))
}

req, err = http.NewRequest("GET", masterConfig.OAuthConfig.MasterPublicURL, nil)
req.Header.Set("Accept", "text/html")
resp, err = transport.RoundTrip(req)
// Create fake config map to test console redirect
webconsoleConfigScheme := runtime.NewScheme()
webconsoleConfigCodecs := serializer.NewCodecFactory(webconsoleConfigScheme)
webConsoleURL := "https://127.0.0.42/console"

if err := webconsoleconfigv1.AddToScheme(webconsoleConfigScheme); err != nil {
t.Fatalf("Unextecpted error: %v", err)
}

clusterAdminKubeClientset, err := testutil.GetClusterAdminKubeClient(clusterAdminKubeConfig)
if err != nil {
t.Fatalf("Unextecpted error: %v", err)
}

ns := kapi.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "openshift-web-console",
},
}
if _, err = clusterAdminKubeClientset.Core().Namespaces().Create(&ns); err != nil {
t.Fatalf("Unextecpted error: %v", err)
}

consoleConfig := webconsoleconfigv1.WebConsoleConfiguration{
ClusterInfo: webconsoleconfigv1.ClusterInfo{
ConsolePublicURL: webConsoleURL,
},
}
data, err := runtime.Encode(webconsoleConfigCodecs.LegacyCodec(webconsoleconfigv1.SchemeGroupVersion), &consoleConfig)
if err != nil {
t.Errorf("Unexpected error: %v", err)
t.Fatalf("Unextecpted error: %v", err)
}
configMap := kapi.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: "openshift-web-console",
Name: "webconsole-config",
},
Data: map[string]string{
"webconsole-config.yaml": string(data),
},
}
if _, err = clusterAdminKubeClientset.Core().ConfigMaps("openshift-web-console").Create(&configMap); err != nil {
t.Fatalf("Unextecpted error: %v", err)
}

// try three times then give up
for i := 0; i < 3; i++ {
req, err = http.NewRequest("GET", masterConfig.OAuthConfig.MasterPublicURL, nil)
req.Header.Set("Accept", "text/html")
resp, err = transport.RoundTrip(req)
if err != nil {
t.Errorf("Unexpected error: %v", err)
break
}
if resp.StatusCode != http.StatusServiceUnavailable {
break
}
var retryAfter int
if h := resp.Header.Get("Retry-After"); len(h) > 0 {
retryAfter, _ = strconv.Atoi(h)
}
// wait at least 1 second
if retryAfter < 1 {
retryAfter = 1
}
t.Errorf("%v: Retry in %d seconds", time.Now(), retryAfter)
time.Sleep(time.Duration(retryAfter) * time.Second)
}
if resp.StatusCode != http.StatusFound {
t.Errorf("Expected %d, got %d", http.StatusFound, resp.StatusCode)
}
if resp.Header.Get("Location") != masterConfig.OAuthConfig.AssetPublicURL {
t.Errorf("Expected %s, got %s", masterConfig.OAuthConfig.AssetPublicURL, resp.Header.Get("Location"))
if resp.Header.Get("Location") != webConsoleURL {
t.Errorf("Expected %s, got %s", webConsoleURL, resp.Header.Get("Location"))
}

// TODO add a test for when asset config is nil, the redirect should not occur in this case even when
Expand Down

0 comments on commit 35f2751

Please sign in to comment.