Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an ssh tunnel option to the /proxy endpoint #9292

Merged
merged 10 commits into from
Jun 8, 2015
7 changes: 6 additions & 1 deletion cluster/gce/configure-vm.sh
Expand Up @@ -220,9 +220,12 @@ mount-master-pd() {
mkdir -p /mnt/master-pd/srv/kubernetes
# Contains the cluster's initial config parameters and auth tokens
mkdir -p /mnt/master-pd/srv/salt-overlay
# Directory for kube-apiserver to store SSH key (if necessary)
mkdir -p /mnt/master-pd/srv/sshproxy

ln -s -f /mnt/master-pd/var/etcd /var/etcd
ln -s -f /mnt/master-pd/srv/kubernetes /srv/kubernetes
ln -s -f /mnt/master-pd/srv/sshproxy /srv/sshproxy
ln -s -f /mnt/master-pd/srv/salt-overlay /srv/salt-overlay

# This is a bit of a hack to get around the fact that salt has to run after the
Expand Down Expand Up @@ -487,16 +490,18 @@ grains:
cbr-cidr: ${MASTER_IP_RANGE}
cloud: gce
EOF
if ! [[ -z "${PROJECT_ID:-}" ]] && ! [[ -z "${TOKEN_URL:-}" ]]; then
if ! [[ -z "${PROJECT_ID:-}" ]] && ! [[ -z "${TOKEN_URL:-}" ]] && ! [[ -z "${NODE_NETWORK:-}" ]] ; then
cat <<EOF >/etc/gce.conf
[global]
token-url = ${TOKEN_URL}
project-id = ${PROJECT_ID}
network-name = ${NODE_NETWORK}
EOF
EXTERNAL_IP=$(curl --fail --silent -H 'Metadata-Flavor: Google' "http://metadata/computeMetadata/v1/instance/network-interfaces/0/access-configs/0/external-ip")
cat <<EOF >>/etc/salt/minion.d/grains.conf
cloud_config: /etc/gce.conf
advertise_address: '${EXTERNAL_IP}'
proxy_ssh_user: '${INSTANCE_PREFIX}'
EOF
fi
}
Expand Down
3 changes: 0 additions & 3 deletions cluster/kubectl.sh
Expand Up @@ -102,9 +102,6 @@ kubectl="${KUBECTL_PATH:-${kubectl}}"

if [[ "$KUBERNETES_PROVIDER" == "gke" ]]; then
detect-project &> /dev/null
config=(
"--context=gke_${PROJECT}_${ZONE}_${CLUSTER_NAME}"
)
elif [[ "$KUBERNETES_PROVIDER" == "ubuntu" || "$KUBERNETES_PROVIDER" == "juju" ]]; then
detect-master > /dev/null
config=(
Expand Down
34 changes: 22 additions & 12 deletions cluster/saltbase/salt/kube-apiserver/kube-apiserver.manifest
Expand Up @@ -5,28 +5,29 @@

{% set cloud_provider = "" -%}
{% set cloud_config = "" -%}
{% set cloud_config_mount = "" -%}
{% set cloud_config_volume = "" -%}

{% if grains.cloud is defined -%}
{% set cloud_provider = "--cloud_provider=" + grains.cloud -%}
{% set cloud_provider = "--cloud_provider=" + grains.cloud -%}

{% if grains.cloud == 'gce' -%}
{% if grains.cloud_config is defined -%}
{% if grains.cloud in [ 'aws', 'gce' ] and grains.cloud_config is defined -%}
{% set cloud_config = "--cloud_config=" + grains.cloud_config -%}
{% set cloud_config_mount = "{\"name\": \"cloudconfigmount\",\"mountPath\": \"" + grains.cloud_config + "\", \"readOnly\": true}," -%}
{% set cloud_config_volume = "{\"name\": \"cloudconfigmount\",\"hostPath\": {\"path\": \"" + grains.cloud_config + "\"}}," -%}
{% endif -%}

{% elif grains.cloud == 'aws' -%}
{% if grains.cloud_config is defined -%}
{% set cloud_config = "--cloud_config=" + grains.cloud_config -%}
{% endif -%}
{% endif -%}

{% endif -%}

{% set advertise_address = "" -%}
{% if grains.advertise_address is defined -%}
{% set advertise_address = "--advertise-address=" + grains.advertise_address -%}
{% endif -%}

{% set proxy_ssh_options = "" -%}
{% if grains.proxy_ssh_user is defined -%}
{% set proxy_ssh_options = "--ssh-user=" + grains.proxy_ssh_user + " --ssh-keyfile=/srv/sshproxy/.sshkeyfile" -%}
{% endif -%}

{% set address = "--address=127.0.0.1" -%}

{% set cluster_name = "" -%}
Expand Down Expand Up @@ -85,7 +86,7 @@
{% endif -%}

{% set params = address + " " + etcd_servers + " " + cloud_provider + " " + cloud_config + " " + runtime_config + " " + admission_control + " " + service_cluster_ip_range + " " + client_ca_file + " " + basic_auth_file + " " + min_request_timeout -%}
{% set params = params + " " + cluster_name + " " + cert_file + " " + key_file + " --secure_port=" + secure_port + " " + token_auth_file + " " + bind_address + " " + pillar['log_level'] + " " + advertise_address -%}
{% set params = params + " " + cluster_name + " " + cert_file + " " + key_file + " --secure_port=" + secure_port + " " + token_auth_file + " " + bind_address + " " + pillar['log_level'] + " " + advertise_address + " " + proxy_ssh_options -%}

{
"apiVersion": "v1beta3",
Expand All @@ -111,6 +112,7 @@
"hostPort": 8080}
],
"volumeMounts": [
{{cloud_config_mount}}
{ "name": "srvkube",
"mountPath": "/srv/kubernetes",
"readOnly": true},
Expand Down Expand Up @@ -140,11 +142,15 @@
"readOnly": true},
{ "name": "etcpkitls",
"mountPath": "/etc/pki/tls",
"readOnly": true}
"readOnly": true},
{ "name": "srvsshproxy",
"mountPath": "/srv/sshproxy",
"readOnly": false}
]
}
],
"volumes":[
{{cloud_config_volume}}
{ "name": "srvkube",
"hostPath": {
"path": "/srv/kubernetes"}
Expand Down Expand Up @@ -184,6 +190,10 @@
{ "name": "etcpkitls",
"hostPath": {
"path": "/etc/pki/tls"}
},
{ "name": "srvsshproxy",
"hostPath": {
"path": "/srv/sshproxy"}
}
]
}}
Expand Up @@ -14,13 +14,17 @@

{% set cloud_provider = "" -%}
{% set cloud_config = "" -%}
{% set cloud_config_mount = "" -%}
{% set cloud_config_volume = "" -%}

{% if grains.cloud is defined -%}
{% set cloud_provider = "--cloud_provider=" + grains.cloud -%}
{% set service_account_key = " --service_account_private_key_file=/srv/kubernetes/server.key " -%}

{% if grains.cloud in [ 'aws', 'gce' ] and grains.cloud_config is defined -%}
{% set cloud_config = "--cloud_config=" + grains.cloud_config -%}
{% set cloud_config_mount = "{\"name\": \"cloudconfigmount\",\"mountPath\": \"" + grains.cloud_config + "\", \"readOnly\": true}," -%}
{% set cloud_config_volume = "{\"name\": \"cloudconfigmount\",\"hostPath\": {\"path\": \"" + grains.cloud_config + "\"}}," -%}
{% endif -%}
{% endif -%}

Expand All @@ -42,6 +46,7 @@
"/usr/local/bin/kube-controller-manager {{params}} 1>>/var/log/kube-controller-manager.log 2>&1"
],
"volumeMounts": [
{{cloud_config_mount}}
{ "name": "srvkube",
"mountPath": "/srv/kubernetes",
"readOnly": true},
Expand Down Expand Up @@ -76,6 +81,7 @@
}
],
"volumes":[
{{cloud_config_volume}}
{ "name": "srvkube",
"hostPath": {
"path": "/srv/kubernetes"}
Expand Down
14 changes: 13 additions & 1 deletion cmd/kube-apiserver/app/server.go
Expand Up @@ -97,6 +97,8 @@ type APIServer struct {
MaxRequestsInFlight int
MinRequestTimeout int
LongRunningRequestRE string
SSHUser string
SSHKeyfile string
}

// NewAPIServer creates a new APIServer object with default parameters
Expand Down Expand Up @@ -201,6 +203,8 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) {
fs.IntVar(&s.MaxRequestsInFlight, "max-requests-inflight", 400, "The maximum number of requests in flight at a given time. When the server exceeds this, it rejects requests. Zero for no limit.")
fs.IntVar(&s.MinRequestTimeout, "min-request-timeout", 1800, "An optional field indicating the minimum number of seconds a handler must keep a request open before timing it out. Currently only honored by the watch request handler, which picks a randomized value above this number as the connection timeout, to spread out load.")
fs.StringVar(&s.LongRunningRequestRE, "long-running-request-regexp", "[.*\\/watch$][^\\/proxy.*]", "A regular expression matching long running requests which should be excluded from maximum inflight request handling.")
fs.StringVar(&s.SSHUser, "ssh-user", "", "If non-empty, use secure SSH proxy to the nodes, using this user name")
fs.StringVar(&s.SSHKeyfile, "ssh-keyfile", "", "If non-empty, use secure SSH proxy to the nodes, using this user keyfile")
}

// TODO: Longer term we should read this from some config store, rather than a flag.
Expand Down Expand Up @@ -352,7 +356,12 @@ func (s *APIServer) Run(_ []string) error {
}
}
}

var installSSH master.InstallSSHKey
if cloud != nil {
if instances, supported := cloud.Instances(); supported {
installSSH = instances.AddSSHKeyToAllInstances
}
}
config := &master.Config{
EtcdHelper: helper,
EventTTL: s.EventTTL,
Expand All @@ -378,6 +387,9 @@ func (s *APIServer) Run(_ []string) error {
ClusterName: s.ClusterName,
ExternalHost: s.ExternalHost,
MinRequestTimeout: s.MinRequestTimeout,
SSHUser: s.SSHUser,
SSHKeyfile: s.SSHKeyfile,
InstallSSHKey: installSSH,
}
m := master.New(config)

Expand Down
5 changes: 3 additions & 2 deletions pkg/apiserver/api_installer.go
Expand Up @@ -18,6 +18,7 @@ package apiserver

import (
"fmt"
"net"
"net/http"
"net/url"
gpath "path"
Expand Down Expand Up @@ -55,14 +56,14 @@ type action struct {
var errEmptyName = errors.NewBadRequest("name must be provided")

// Installs handlers for API resources.
func (a *APIInstaller) Install() (ws *restful.WebService, errors []error) {
func (a *APIInstaller) Install(proxyDialer func(network, addr string) (net.Conn, error)) (ws *restful.WebService, errors []error) {
errors = make([]error, 0)

// Create the WebService.
ws = a.newWebService()

redirectHandler := (&RedirectHandler{a.group.Storage, a.group.Codec, a.group.Context, a.info})
proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.group.Storage, a.group.Codec, a.group.Context, a.info})
proxyHandler := (&ProxyHandler{a.prefix + "/proxy/", a.group.Storage, a.group.Codec, a.group.Context, a.info, proxyDialer})

// Register the paths in a deterministic (sorted) order to get a deterministic swagger spec.
paths := make([]string, len(a.group.Storage))
Expand Down
5 changes: 3 additions & 2 deletions pkg/apiserver/apiserver.go
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"path"
"strconv"
Expand Down Expand Up @@ -149,7 +150,7 @@ type RestContainer struct {
// InstallREST registers the REST handlers (storage, watch, proxy and redirect) into a restful Container.
// It is expected that the provided path root prefix will serve all operations. Root MUST NOT end
// in a slash. A restful WebService is created for the group and version.
func (g *APIGroupVersion) InstallREST(container *RestContainer) error {
func (g *APIGroupVersion) InstallREST(container *RestContainer, proxyDialer func(network, addr string) (net.Conn, error)) error {
info := &APIRequestInfoResolver{util.NewStringSet(strings.TrimPrefix(g.Root, "/")), g.Mapper}

prefix := path.Join(g.Root, g.Version)
Expand All @@ -159,7 +160,7 @@ func (g *APIGroupVersion) InstallREST(container *RestContainer) error {
prefix: prefix,
minRequestTimeout: container.MinRequestTimeout,
}
ws, registrationErrors := installer.Install()
ws, registrationErrors := installer.Install(proxyDialer)
container.Add(ws)
return errors.NewAggregate(registrationErrors)
}
Expand Down
6 changes: 3 additions & 3 deletions pkg/apiserver/apiserver_test.go
Expand Up @@ -231,7 +231,7 @@ func handleInternal(legacy bool, storage map[string]rest.Storage, admissionContr
container := restful.NewContainer()
container.Router(restful.CurlyRouter{})
mux := container.ServeMux
if err := group.InstallREST(&RestContainer{container, 0}); err != nil {
if err := group.InstallREST(&RestContainer{container, 0}, nil); err != nil {
panic(fmt.Sprintf("unable to install container %s: %v", group.Version, err))
}
ws := new(restful.WebService)
Expand Down Expand Up @@ -1901,7 +1901,7 @@ func TestParentResourceIsRequired(t *testing.T) {
Codec: newCodec,
}
container := restful.NewContainer()
if err := group.InstallREST(&RestContainer{container, 0}); err == nil {
if err := group.InstallREST(&RestContainer{container, 0}, nil); err == nil {
t.Fatal("expected error")
}

Expand Down Expand Up @@ -1929,7 +1929,7 @@ func TestParentResourceIsRequired(t *testing.T) {
Codec: newCodec,
}
container = restful.NewContainer()
if err := group.InstallREST(&RestContainer{container, 0}); err != nil {
if err := group.InstallREST(&RestContainer{container, 0}, nil); err != nil {
t.Fatal(err)
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/apiserver/proxy.go
Expand Up @@ -49,6 +49,8 @@ type ProxyHandler struct {
codec runtime.Codec
context api.RequestContextMapper
apiRequestInfoResolver *APIRequestInfoResolver

dial func(network, addr string) (net.Conn, error)
}

func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
Expand Down Expand Up @@ -119,6 +121,10 @@ func (r *ProxyHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
httpCode = http.StatusNotFound
return
}
// If we have a custom dialer, and no pre-existing transport, initialize it to use the dialer.
if transport == nil && r.dial != nil {
transport = &http.Transport{Dial: r.dial}
}

// Default to http
if location.Scheme == "" {
Expand Down
3 changes: 3 additions & 0 deletions pkg/client/helper.go
Expand Up @@ -102,6 +102,9 @@ type KubeletConfig struct {

// HTTPTimeout is used by the client to timeout http requests to Kubelet.
HTTPTimeout time.Duration

// Dial is a custom dialer used for the client
Dial func(net, addr string) (net.Conn, error)
}

// TLSClientConfig contains settings to enable transport layer security
Expand Down
23 changes: 16 additions & 7 deletions pkg/client/kubelet.go
Expand Up @@ -45,14 +45,12 @@ type ConnectionInfoGetter interface {
// HTTPKubeletClient is the default implementation of KubeletHealthchecker, accesses the kubelet over HTTP.
type HTTPKubeletClient struct {
Client *http.Client
Config *KubeletConfig
Port uint
EnableHttps bool
}

// TODO: this structure is questionable, it should be using client.Config and overriding defaults.
func NewKubeletClient(config *KubeletConfig) (KubeletClient, error) {
transport := http.DefaultTransport

func MakeTransport(config *KubeletConfig) (http.RoundTripper, error) {
cfg := &Config{TLSClientConfig: config.TLSClientConfig}
if config.EnableHttps {
hasCA := len(config.CAFile) > 0 || len(config.CAData) > 0
Expand All @@ -64,18 +62,29 @@ func NewKubeletClient(config *KubeletConfig) (KubeletClient, error) {
if err != nil {
return nil, err
}
if tlsConfig != nil {
transport = &http.Transport{
if config.Dial != nil || tlsConfig != nil {
return &http.Transport{
Dial: config.Dial,
TLSClientConfig: tlsConfig,
}
}, nil
} else {
return http.DefaultTransport, nil
}
}

// TODO: this structure is questionable, it should be using client.Config and overriding defaults.
func NewKubeletClient(config *KubeletConfig) (KubeletClient, error) {
transport, err := MakeTransport(config)
if err != nil {
return nil, err
}
c := &http.Client{
Transport: transport,
Timeout: config.HTTPTimeout,
}
return &HTTPKubeletClient{
Client: c,
Config: config,
Port: config.Port,
EnableHttps: config.EnableHttps,
}, nil
Expand Down
4 changes: 4 additions & 0 deletions pkg/cloudprovider/aws/aws.go
Expand Up @@ -230,6 +230,10 @@ func newEc2Filter(name string, value string) *ec2.Filter {
return filter
}

func (self *AWSCloud) AddSSHKeyToAllInstances(user string, keyData []byte) error {
return errors.New("unimplemented")
}

// Implementation of EC2.Instances
func (self *awsSdkEC2) DescribeInstances(request *ec2.DescribeInstancesInput) ([]*ec2.Instance, error) {
// Instances are paged
Expand Down
3 changes: 3 additions & 0 deletions pkg/cloudprovider/cloud.go
Expand Up @@ -108,6 +108,9 @@ type Instances interface {
List(filter string) ([]string, error)
// GetNodeResources gets the resources for a particular node
GetNodeResources(name string) (*api.NodeResources, error)
// AddSSHKeyToAllInstances adds an SSH public key as a legal identity for all instances
// expected format for the key is standard ssh-keygen format: <protocol> <blob>
AddSSHKeyToAllInstances(user string, keyData []byte) error
}

// Route is a representation of an advanced routing rule.
Expand Down