diff --git a/cmd/origin-web-console/console.go b/cmd/origin-web-console/console.go new file mode 100644 index 000000000000..7534c007b787 --- /dev/null +++ b/cmd/origin-web-console/console.go @@ -0,0 +1,37 @@ +package main + +import ( + "flag" + "math/rand" + "os" + "runtime" + "time" + + "github.com/golang/glog" + + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/kubernetes/pkg/util/logs" + + webconsolecmd "github.com/openshift/origin/pkg/assets/apiserver/cmd" + "github.com/openshift/origin/pkg/cmd/util/serviceability" +) + +func main() { + rand.Seed(time.Now().UTC().UnixNano()) + + logs.InitLogs() + defer logs.FlushLogs() + + if len(os.Getenv("GOMAXPROCS")) == 0 { + runtime.GOMAXPROCS(runtime.NumCPU()) + } + + defer serviceability.BehaviorOnPanic(os.Getenv("OPENSHIFT_ON_PANIC"))() + defer serviceability.Profile(os.Getenv("OPENSHIFT_PROFILE")).Stop() + + cmd := webconsolecmd.NewCommandStartWebConsoleServer(os.Stdout, os.Stderr, wait.NeverStop) + cmd.Flags().AddGoFlagSet(flag.CommandLine) + if err := cmd.Execute(); err != nil { + glog.Fatal(err) + } +} diff --git a/hack/lib/build/constants.sh b/hack/lib/build/constants.sh index c6f699a87b40..9f824b4246d4 100755 --- a/hack/lib/build/constants.sh +++ b/hack/lib/build/constants.sh @@ -40,6 +40,7 @@ readonly OS_CROSS_COMPILE_TARGETS=( cmd/oc cmd/kubefed cmd/template-service-broker + cmd/origin-web-console ) readonly OS_CROSS_COMPILE_BINARIES=("${OS_CROSS_COMPILE_TARGETS[@]##*/}") diff --git a/install/origin-web-console/apiserver-config.yaml b/install/origin-web-console/apiserver-config.yaml new file mode 100644 index 000000000000..8f3f87c0b314 --- /dev/null +++ b/install/origin-web-console/apiserver-config.yaml @@ -0,0 +1,21 @@ +kind: AssetConfig +apiVersion: v1 +extensionDevelopment: false +extensionProperties: null +extensionScripts: null +extensionStylesheets: null +extensions: null +loggingPublicURL: "" +logoutURL: "" +masterPublicURL: https://127.0.0.1:8443 +metricsPublicURL: "" +publicURL: https://127.0.0.1:8443/console/ +servingInfo: + bindAddress: 0.0.0.0:8443 + bindNetwork: tcp4 + certFile: /var/serving-cert/tls.crt + clientCA: "" + keyFile: /var/serving-cert/tls.key + maxRequestsInFlight: 0 + namedCertificates: null + requestTimeoutSeconds: 0 \ No newline at end of file diff --git a/install/origin-web-console/apiserver-template.yaml b/install/origin-web-console/apiserver-template.yaml new file mode 100644 index 000000000000..e94155264219 --- /dev/null +++ b/install/origin-web-console/apiserver-template.yaml @@ -0,0 +1,94 @@ +apiVersion: template.openshift.io/v1 +kind: Template +metadata: + name: template-service-broker-webconsole +parameters: +- name: IMAGE + value: openshift/origin-web-console:latest +- name: NAMESPACE + value: openshift-web-console +- name: LOGLEVEL + value: "0" +- name: API_SERVER_CONFIG +- name: NODE_SELECTOR + value: "{}" +objects: + +# to create the web console server +- apiVersion: extensions/v1beta1 + kind: DaemonSet + metadata: + namespace: ${NAMESPACE} + name: webconsole + labels: + webconsole: "true" + spec: + template: + metadata: + name: webconsole + labels: + webconsole: "true" + spec: + serviceAccountName: webconsole + containers: + - name: c + image: ${IMAGE} + imagePullPolicy: IfNotPresent + command: + - "/usr/bin/origin-web-console" + - "--audit-log-path=-" + - "--config=/var/webconsole-config/webconsole-config.yaml" + ports: + - containerPort: 8443 + volumeMounts: + - mountPath: /var/serving-cert + name: serving-cert + - mountPath: /var/webconsole-config + name: webconsole-config + readinessProbe: + httpGet: + path: /healthz + port: 8443 + scheme: HTTPS + nodeSelector: "${{NODE_SELECTOR}}" + volumes: + - name: serving-cert + secret: + defaultMode: 420 + secretName: webconsole-serving-cert + - name: webconsole-config + configMap: + defaultMode: 420 + name: webconsole-config + +# to create the config for the web console +- apiVersion: v1 + kind: ConfigMap + metadata: + namespace: ${NAMESPACE} + name: webconsole-config + data: + webconsole-config.yaml: ${API_SERVER_CONFIG} + +# to be able to assign powers to the process +- apiVersion: v1 + kind: ServiceAccount + metadata: + namespace: ${NAMESPACE} + name: webconsole + +# to be able to expose web console inside the cluster +- apiVersion: v1 + kind: Service + metadata: + namespace: ${NAMESPACE} + name: webconsole + annotations: + service.alpha.openshift.io/serving-cert-secret-name: webconsole-serving-cert + spec: + selector: + webconsole: "true" + ports: + - name: https + port: 443 + targetPort: 8443 diff --git a/pkg/assets/apiserver/asset_apiserver.go b/pkg/assets/apiserver/asset_apiserver.go index 2ef4acbb35f1..4b9d0f963381 100644 --- a/pkg/assets/apiserver/asset_apiserver.go +++ b/pkg/assets/apiserver/asset_apiserver.go @@ -72,7 +72,7 @@ func NewAssetServerConfig(assetConfig oapi.AssetConfig) (*AssetServerConfig, err if err != nil { return nil, err } - secureServingOptions := genericapiserveroptions.SecureServingOptions{} + secureServingOptions := genericapiserveroptions.NewSecureServingOptions() secureServingOptions.BindPort = port secureServingOptions.ServerCert.CertKey.CertFile = assetConfig.ServingInfo.ServerCert.CertFile secureServingOptions.ServerCert.CertKey.KeyFile = assetConfig.ServingInfo.ServerCert.KeyFile diff --git a/pkg/assets/apiserver/cmd/start.go b/pkg/assets/apiserver/cmd/start.go new file mode 100644 index 000000000000..e8fa279f3876 --- /dev/null +++ b/pkg/assets/apiserver/cmd/start.go @@ -0,0 +1,153 @@ +package cmd + +import ( + "fmt" + "io" + "io/ioutil" + + "github.com/golang/glog" + "github.com/spf13/cobra" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + genericapiserver "k8s.io/apiserver/pkg/server" + genericoptions "k8s.io/apiserver/pkg/server/options" + "k8s.io/kubernetes/pkg/kubectl/cmd/util" + + webconsoleserver "github.com/openshift/origin/pkg/assets/apiserver" + configapi "github.com/openshift/origin/pkg/cmd/server/api" + configapiinstall "github.com/openshift/origin/pkg/cmd/server/api/install" + configapivalidation "github.com/openshift/origin/pkg/cmd/server/api/validation" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +type WebConsoleServerOptions struct { + // we don't have any storage, so we shouldn't use the recommended options + Audit *genericoptions.AuditOptions + Features *genericoptions.FeatureOptions + + StdOut io.Writer + StdErr io.Writer + + WebConsoleConfig *configapi.AssetConfig +} + +func NewWebConsoleServerOptions(out, errOut io.Writer) *WebConsoleServerOptions { + o := &WebConsoleServerOptions{ + Audit: genericoptions.NewAuditOptions(), + Features: genericoptions.NewFeatureOptions(), + + StdOut: out, + StdErr: errOut, + } + + return o +} + +func NewCommandStartWebConsoleServer(out, errOut io.Writer, stopCh <-chan struct{}) *cobra.Command { + o := NewWebConsoleServerOptions(out, errOut) + + cmd := &cobra.Command{ + Use: "origin-web-console", + Short: "Launch a web console server", + Long: "Launch a web console server", + RunE: func(c *cobra.Command, args []string) error { + if err := o.Complete(c); err != nil { + return err + } + if err := o.Validate(args); err != nil { + return err + } + if err := o.RunWebConsoleServer(stopCh); err != nil { + return err + } + return nil + }, + } + + flags := cmd.Flags() + o.Audit.AddFlags(flags) + o.Features.AddFlags(flags) + flags.String("config", "", "filename containing the WebConsoleConfig") + + return cmd +} + +func (o WebConsoleServerOptions) Validate(args []string) error { + if o.WebConsoleConfig == nil { + return fmt.Errorf("missing config: specify --config") + } + + validationResults := configapivalidation.ValidateAssetConfig(o.WebConsoleConfig, field.NewPath("config")) + if len(validationResults.Warnings) != 0 { + for _, warning := range validationResults.Warnings { + glog.Warningf("Warning: %v, web console start will continue.", warning) + } + } + if len(validationResults.Errors) != 0 { + return apierrors.NewInvalid(configapi.Kind("AssetConfig"), "", validationResults.Errors) + } + + return nil +} + +func (o *WebConsoleServerOptions) Complete(cmd *cobra.Command) error { + configFile := util.GetFlagString(cmd, "config") + if len(configFile) > 0 { + content, err := ioutil.ReadFile(configFile) + if err != nil { + return err + } + configObj, err := runtime.Decode(configCodecs.UniversalDecoder(), content) + if err != nil { + return err + } + config, ok := configObj.(*configapi.AssetConfig) + if !ok { + return fmt.Errorf("unexpected type: %T", configObj) + } + o.WebConsoleConfig = config + } + + return nil +} + +func (o WebConsoleServerOptions) Config() (*webconsoleserver.AssetServerConfig, error) { + serverConfig, err := webconsoleserver.NewAssetServerConfig(*o.WebConsoleConfig) + if err != nil { + return nil, err + } + + if err := o.Audit.ApplyTo(serverConfig.GenericConfig); err != nil { + return nil, err + } + if err := o.Features.ApplyTo(serverConfig.GenericConfig); err != nil { + return nil, err + } + + return serverConfig, nil +} + +func (o WebConsoleServerOptions) RunWebConsoleServer(stopCh <-chan struct{}) error { + config, err := o.Config() + if err != nil { + return err + } + + server, err := config.Complete().New(genericapiserver.EmptyDelegate) + if err != nil { + return err + } + return server.GenericAPIServer.PrepareRun().Run(stopCh) +} + +// these are used to set up for reading the config +var ( + configScheme = runtime.NewScheme() + configCodecs = serializer.NewCodecFactory(configScheme) +) + +func init() { + configapiinstall.AddToScheme(configScheme) +} diff --git a/pkg/cmd/server/api/install/install.go b/pkg/cmd/server/api/install/install.go index 2554dab6a244..207ae35e89f2 100644 --- a/pkg/cmd/server/api/install/install.go +++ b/pkg/cmd/server/api/install/install.go @@ -4,6 +4,7 @@ import ( "fmt" "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/apis/apiserver" apiserverv1alpha1 "k8s.io/apiserver/pkg/apis/apiserver/v1alpha1" @@ -29,14 +30,18 @@ var accessor = meta.NewAccessor() var availableVersions = []schema.GroupVersion{configapiv1.SchemeGroupVersion} func init() { - configapi.AddToScheme(configapi.Scheme) - configapiv1.AddToScheme(configapi.Scheme) + AddToScheme(configapi.Scheme) +} + +func AddToScheme(scheme *runtime.Scheme) { + configapi.AddToScheme(scheme) + configapiv1.AddToScheme(scheme) // we additionally need to enable audit versions, since we embed the audit // policy file inside master-config.yaml - audit.AddToScheme(configapi.Scheme) - auditv1alpha1.AddToScheme(configapi.Scheme) - apiserver.AddToScheme(configapi.Scheme) - apiserverv1alpha1.AddToScheme(configapi.Scheme) + audit.AddToScheme(scheme) + auditv1alpha1.AddToScheme(scheme) + apiserver.AddToScheme(scheme) + apiserverv1alpha1.AddToScheme(scheme) } func interfacesFor(version schema.GroupVersion) (*meta.VersionInterfaces, error) { diff --git a/pkg/cmd/server/api/register.go b/pkg/cmd/server/api/register.go index 9f793371bf9d..b630e9d5875c 100644 --- a/pkg/cmd/server/api/register.go +++ b/pkg/cmd/server/api/register.go @@ -39,6 +39,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &MasterConfig{}, &NodeConfig{}, + &AssetConfig{}, &SessionSecrets{}, &BasicAuthPasswordIdentityProvider{}, diff --git a/pkg/cmd/server/api/types.go b/pkg/cmd/server/api/types.go index 8e22abe2e67a..68caf1253dd7 100644 --- a/pkg/cmd/server/api/types.go +++ b/pkg/cmd/server/api/types.go @@ -824,6 +824,8 @@ type DNSConfig struct { } type AssetConfig struct { + metav1.TypeMeta + ServingInfo HTTPServingInfo // PublicURL is where you can find the asset server (TODO do we really need this?) diff --git a/pkg/cmd/server/api/v1/register.go b/pkg/cmd/server/api/v1/register.go index a52006044e5a..4832f724463d 100644 --- a/pkg/cmd/server/api/v1/register.go +++ b/pkg/cmd/server/api/v1/register.go @@ -20,6 +20,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &MasterConfig{}, &NodeConfig{}, + &AssetConfig{}, &SessionSecrets{}, &BasicAuthPasswordIdentityProvider{}, diff --git a/pkg/cmd/server/api/v1/types.go b/pkg/cmd/server/api/v1/types.go index 069442a2518e..0b1c81dea105 100644 --- a/pkg/cmd/server/api/v1/types.go +++ b/pkg/cmd/server/api/v1/types.go @@ -713,8 +713,12 @@ type DNSConfig struct { AllowRecursiveQueries bool `json:"allowRecursiveQueries"` } +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + // AssetConfig holds the necessary configuration options for serving assets type AssetConfig struct { + metav1.TypeMeta `json:",inline"` + // ServingInfo is the HTTP serving information for these assets ServingInfo HTTPServingInfo `json:"servingInfo"` diff --git a/pkg/cmd/server/origin/master.go b/pkg/cmd/server/origin/master.go index 24d07f7718e4..a210680ad4b0 100644 --- a/pkg/cmd/server/origin/master.go +++ b/pkg/cmd/server/origin/master.go @@ -16,6 +16,8 @@ import ( kubeapiserver "k8s.io/kubernetes/pkg/master" kcorestorage "k8s.io/kubernetes/pkg/registry/core/rest" + "io/ioutil" + assetapiserver "github.com/openshift/origin/pkg/assets/apiserver" configapi "github.com/openshift/origin/pkg/cmd/server/api" "github.com/openshift/origin/pkg/cmd/server/bootstrappolicy" @@ -153,6 +155,19 @@ func (c *MasterConfig) newAssetServerHandler(genericConfig *apiserver.Config) (h return http.NotFoundHandler(), nil } + if WebConsoleProxy() { + fmt.Printf("#### installing the webconsole proxy") + caBundle, err := ioutil.ReadFile(c.Options.ControllerConfig.ServiceServingCert.Signer.CertFile) + if err != nil { + return nil, err + } + proxyHandler, err := NewWebConsoleProxyHandler(aggregatorapiserver.NewClusterIPServiceResolver(c.ClientGoKubeInformers.Core().V1().Services().Lister()), caBundle) + if err != nil { + return nil, err + } + return proxyHandler, nil + } + config, err := NewAssetServerConfigFromMasterConfig(c.Options) if err != nil { return nil, err diff --git a/pkg/cmd/server/origin/master_config.go b/pkg/cmd/server/origin/master_config.go index e091dc4908fa..46bd4798ac10 100644 --- a/pkg/cmd/server/origin/master_config.go +++ b/pkg/cmd/server/origin/master_config.go @@ -30,6 +30,7 @@ import ( kubernetes "github.com/openshift/origin/pkg/cmd/server/kubernetes/master" originadmission "github.com/openshift/origin/pkg/cmd/server/origin/admission" originrest "github.com/openshift/origin/pkg/cmd/server/origin/rest" + "github.com/openshift/origin/pkg/cmd/util" imageadmission "github.com/openshift/origin/pkg/image/admission" imageapi "github.com/openshift/origin/pkg/image/apis/image" imageinformer "github.com/openshift/origin/pkg/image/generated/informers/internalversion" @@ -39,6 +40,8 @@ import ( quotainformer "github.com/openshift/origin/pkg/quota/generated/informers/internalversion" userinformer "github.com/openshift/origin/pkg/user/generated/informers/internalversion" + "strings" + securityinformer "github.com/openshift/origin/pkg/security/generated/informers/internalversion" "github.com/openshift/origin/pkg/service" "github.com/openshift/origin/pkg/util/restoptions" @@ -259,3 +262,10 @@ func (c *MasterConfig) WebConsoleEnabled() bool { func (c *MasterConfig) WebConsoleStandalone() bool { return c.Options.AssetConfig.ServingInfo.BindAddress != c.Options.ServingInfo.BindAddress } + +func WebConsoleProxy() bool { + if false { + return strings.EqualFold(util.Env("OS_WEB_CONSOLE_PROXY", "false"), "true") + } + return true +} diff --git a/pkg/cmd/server/origin/webconsole_proxy.go b/pkg/cmd/server/origin/webconsole_proxy.go new file mode 100644 index 000000000000..7914ff4b5aad --- /dev/null +++ b/pkg/cmd/server/origin/webconsole_proxy.go @@ -0,0 +1,92 @@ +package origin + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "k8s.io/apimachinery/pkg/runtime" + utilnet "k8s.io/apimachinery/pkg/util/net" + "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + genericrest "k8s.io/apiserver/pkg/registry/generic/rest" + restclient "k8s.io/client-go/rest" +) + +// A ServiceResolver knows how to get a URL given a service. +type ServiceResolver interface { + ResolveEndpoint(namespace, name string) (*url.URL, error) +} + +// proxyHandler provides a http.Handler which will proxy traffic to locations +// specified by items implementing Redirector. +type webConsoleProxyHandler struct { + // Endpoints based routing to map from cluster IP to routable IP + serviceResolver ServiceResolver + + // proxyRoundTripper is the re-useable portion of the transport. It does not vary with any request. + proxyRoundTripper http.RoundTripper + + restConfig *restclient.Config +} + +const ( + serviceName = "webconsole" + serviceNamespace = "openshift-web-console" +) + +func NewWebConsoleProxyHandler(serviceResolver ServiceResolver, caBundle []byte) (*webConsoleProxyHandler, error) { + restConfig := &restclient.Config{ + TLSClientConfig: restclient.TLSClientConfig{ + ServerName: serviceName + "." + serviceNamespace + ".svc", + CAData: caBundle, + }, + } + proxyRoundTripper, err := restclient.TransportFor(restConfig) + if err != nil { + return nil, err + } + + return &webConsoleProxyHandler{ + serviceResolver: serviceResolver, + proxyRoundTripper: proxyRoundTripper, + restConfig: restConfig, + }, nil +} + +func (r *webConsoleProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + // write a new location based on the existing request pointed at the target service + location := &url.URL{} + location.Scheme = "https" + rloc, err := r.serviceResolver.ResolveEndpoint(serviceNamespace, serviceName) + if err != nil { + http.Error(w, fmt.Sprintf("missing route (%s)", err.Error()), http.StatusInternalServerError) + return + } + location.Host = rloc.Host + location.Path = req.URL.Path + location.RawQuery = req.URL.Query().Encode() + + // WithContext creates a shallow clone of the request with the new context. + newReq := req.WithContext(context.Background()) + newReq.Header = utilnet.CloneHeader(req.Header) + newReq.URL = location + + handler := genericrest.NewUpgradeAwareProxyHandler(location, r.proxyRoundTripper, false, false, &responder{w: w}) + handler.ServeHTTP(w, newReq) +} + +// responder implements rest.Responder for assisting a connector in writing objects or errors. +type responder struct { + w http.ResponseWriter +} + +// TODO this should properly handle content type negotiation +// if the caller asked for protobuf and you write JSON bad things happen. +func (r *responder) Object(statusCode int, obj runtime.Object) { + responsewriters.WriteRawJSON(statusCode, obj, r.w) +} + +func (r *responder) Error(err error) { + http.Error(r.w, err.Error(), http.StatusInternalServerError) +}