diff --git a/api/swagger-spec/api.json b/api/swagger-spec/api.json index b21e436b15e..4a1adaa082b 100644 --- a/api/swagger-spec/api.json +++ b/api/swagger-spec/api.json @@ -1,7 +1,7 @@ { "swaggerVersion": "1.2", "apiVersion": "", - "basePath": "https://10.10.10.10:6443", + "basePath": "https://10.10.10.10:443", "resourcePath": "/api", "apis": [ { diff --git a/api/swagger-spec/apis.json b/api/swagger-spec/apis.json index 07d061d49b3..0d4f510e6b3 100644 --- a/api/swagger-spec/apis.json +++ b/api/swagger-spec/apis.json @@ -1,7 +1,7 @@ { "swaggerVersion": "1.2", "apiVersion": "", - "basePath": "https://10.10.10.10:6443", + "basePath": "https://10.10.10.10:443", "resourcePath": "/apis", "apis": [ { diff --git a/api/swagger-spec/extensions.json b/api/swagger-spec/extensions.json index 7bdf667192d..64978cd96f5 100644 --- a/api/swagger-spec/extensions.json +++ b/api/swagger-spec/extensions.json @@ -1,7 +1,7 @@ { "swaggerVersion": "1.2", "apiVersion": "", - "basePath": "https://10.10.10.10:6443", + "basePath": "https://10.10.10.10:443", "resourcePath": "/apis/extensions", "apis": [ { diff --git a/api/swagger-spec/extensions_v1beta1.json b/api/swagger-spec/extensions_v1beta1.json index dab28d951ef..1f2d68a42c9 100644 --- a/api/swagger-spec/extensions_v1beta1.json +++ b/api/swagger-spec/extensions_v1beta1.json @@ -1,7 +1,7 @@ { "swaggerVersion": "1.2", "apiVersion": "extensions/v1beta1", - "basePath": "https://10.10.10.10:6443", + "basePath": "https://10.10.10.10:443", "resourcePath": "/apis/extensions/v1beta1", "apis": [ { diff --git a/api/swagger-spec/v1.json b/api/swagger-spec/v1.json index 165dd4ec328..bbe8c836869 100644 --- a/api/swagger-spec/v1.json +++ b/api/swagger-spec/v1.json @@ -1,7 +1,7 @@ { "swaggerVersion": "1.2", "apiVersion": "v1", - "basePath": "https://10.10.10.10:6443", + "basePath": "https://10.10.10.10:443", "resourcePath": "/api/v1", "apis": [ { diff --git a/api/swagger-spec/version.json b/api/swagger-spec/version.json index b3e984f4df2..9f281939369 100644 --- a/api/swagger-spec/version.json +++ b/api/swagger-spec/version.json @@ -1,7 +1,7 @@ { "swaggerVersion": "1.2", "apiVersion": "", - "basePath": "https://10.10.10.10:6443", + "basePath": "https://10.10.10.10:443", "resourcePath": "/version", "apis": [ { diff --git a/pkg/api/unversioned/types.go b/pkg/api/unversioned/types.go index 71544493154..4d7a8f7d8e4 100644 --- a/pkg/api/unversioned/types.go +++ b/pkg/api/unversioned/types.go @@ -308,6 +308,14 @@ type APIVersions struct { TypeMeta `json:",inline"` // versions are the api versions that are available. Versions []string `json:"versions"` + // a map of client CIDR to server address that is serving this group. + // This is to help clients reach servers in the most network-efficient way possible. + // Clients can use the appropriate server address as per the CIDR that they match. + // In case of multiple matches, clients should use the longest matching CIDR. + // The server returns only those CIDRs that it thinks that the client can match. + // For example: the master will return an internal IP CIDR only, if the client reaches the server using an internal IP. + // Server looks at X-Forwarded-For header or X-Real-Ip header or request.RemoteAddr (in that order) to get the client IP. + ServerAddressByClientCIDRs []ServerAddressByClientCIDR `json:"serverAddressByClientCIDRs"` } // APIGroupList is a list of APIGroup, to allow clients to discover the API at @@ -329,6 +337,23 @@ type APIGroup struct { // preferredVersion is the version preferred by the API server, which // probably is the storage version. PreferredVersion GroupVersionForDiscovery `json:"preferredVersion,omitempty"` + // a map of client CIDR to server address that is serving this group. + // This is to help clients reach servers in the most network-efficient way possible. + // Clients can use the appropriate server address as per the CIDR that they match. + // In case of multiple matches, clients should use the longest matching CIDR. + // The server returns only those CIDRs that it thinks that the client can match. + // For example: the master will return an internal IP CIDR only, if the client reaches the server using an internal IP. + // Server looks at X-Forwarded-For header or X-Real-Ip header or request.RemoteAddr (in that order) to get the client IP. + ServerAddressByClientCIDRs []ServerAddressByClientCIDR `json:"serverAddressByClientCIDRs"` +} + +// ServerAddressByClientCIDR helps the client to determine the server address that they should use, depending on the clientCIDR that they match. +type ServerAddressByClientCIDR struct { + // The CIDR with which clients can match their IP to figure out the server address that they should use. + ClientCIDR string `json:"clientCIDR"` + // Address of this server, suitable for a client that matches the above CIDR. + // This can be a hostname, hostname:port, IP or IP:port. + ServerAddress string `json:"serverAddress"` } // GroupVersion contains the "group/version" and "version" string of a version. diff --git a/pkg/api/unversioned/types_swagger_doc_generated.go b/pkg/api/unversioned/types_swagger_doc_generated.go index a65e8893187..9df14a044f5 100644 --- a/pkg/api/unversioned/types_swagger_doc_generated.go +++ b/pkg/api/unversioned/types_swagger_doc_generated.go @@ -28,10 +28,11 @@ package unversioned // AUTO-GENERATED FUNCTIONS START HERE var map_APIGroup = map[string]string{ - "": "APIGroup contains the name, the supported versions, and the preferred version of a group.", - "name": "name is the name of the group.", - "versions": "versions are the versions supported in this group.", - "preferredVersion": "preferredVersion is the version preferred by the API server, which probably is the storage version.", + "": "APIGroup contains the name, the supported versions, and the preferred version of a group.", + "name": "name is the name of the group.", + "versions": "versions are the versions supported in this group.", + "preferredVersion": "preferredVersion is the version preferred by the API server, which probably is the storage version.", + "serverAddressByClientCIDRs": "a map of client CIDR to server address that is serving this group. This is to help clients reach servers in the most network-efficient way possible. Clients can use the appropriate server address as per the CIDR that they match. In case of multiple matches, clients should use the longest matching CIDR. The server returns only those CIDRs that it thinks that the client can match. For example: the master will return an internal IP CIDR only, if the client reaches the server using an internal IP. Server looks at X-Forwarded-For header or X-Real-Ip header or request.RemoteAddr (in that order) to get the client IP.", } func (APIGroup) SwaggerDoc() map[string]string { @@ -69,8 +70,9 @@ func (APIResourceList) SwaggerDoc() map[string]string { } var map_APIVersions = map[string]string{ - "": "APIVersions lists the versions that are available, to allow clients to discover the API at /api, which is the root path of the legacy v1 API.", - "versions": "versions are the api versions that are available.", + "": "APIVersions lists the versions that are available, to allow clients to discover the API at /api, which is the root path of the legacy v1 API.", + "versions": "versions are the api versions that are available.", + "serverAddressByClientCIDRs": "a map of client CIDR to server address that is serving this group. This is to help clients reach servers in the most network-efficient way possible. Clients can use the appropriate server address as per the CIDR that they match. In case of multiple matches, clients should use the longest matching CIDR. The server returns only those CIDRs that it thinks that the client can match. For example: the master will return an internal IP CIDR only, if the client reaches the server using an internal IP. Server looks at X-Forwarded-For header or X-Real-Ip header or request.RemoteAddr (in that order) to get the client IP.", } func (APIVersions) SwaggerDoc() map[string]string { @@ -145,6 +147,16 @@ func (RootPaths) SwaggerDoc() map[string]string { return map_RootPaths } +var map_ServerAddressByClientCIDR = map[string]string{ + "": "ServerAddressByClientCIDR helps the client to determine the server address that they should use, depending on the clientCIDR that they match.", + "clientCIDR": "The CIDR with which clients can match their IP to figure out the server address that they should use.", + "serverAddress": "Address of this server, suitable for a client that matches the above CIDR. This can be a hostname, hostname:port, IP or IP:port.", +} + +func (ServerAddressByClientCIDR) SwaggerDoc() map[string]string { + return map_ServerAddressByClientCIDR +} + var map_Status = map[string]string{ "": "Status is a return value for calls that don't return other objects.", "metadata": "Standard list metadata. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds", diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 8700e106043..47bb8ad3c82 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -218,10 +218,10 @@ func serviceErrorHandler(s runtime.NegotiatedSerializer, requestResolver *Reques } // Adds a service to return the supported api versions at the legacy /api. -func AddApiWebService(s runtime.NegotiatedSerializer, container *restful.Container, apiPrefix string, versions []string) { +func AddApiWebService(s runtime.NegotiatedSerializer, container *restful.Container, apiPrefix string, getAPIVersionsFunc func(req *restful.Request) *unversioned.APIVersions) { // TODO: InstallREST should register each version automatically - versionHandler := APIVersionHandler(s, versions[:]...) + versionHandler := APIVersionHandler(s, getAPIVersionsFunc) ws := new(restful.WebService) ws.Path(apiPrefix) ws.Doc("get available API versions") @@ -234,7 +234,7 @@ func AddApiWebService(s runtime.NegotiatedSerializer, container *restful.Contain } // Adds a service to return the supported api versions at /apis. -func AddApisWebService(s runtime.NegotiatedSerializer, container *restful.Container, apiPrefix string, f func() []unversioned.APIGroup) { +func AddApisWebService(s runtime.NegotiatedSerializer, container *restful.Container, apiPrefix string, f func(req *restful.Request) []unversioned.APIGroup) { rootAPIHandler := RootAPIHandler(s, f) ws := new(restful.WebService) ws.Path(apiPrefix) @@ -279,16 +279,16 @@ func handleVersion(req *restful.Request, resp *restful.Response) { } // APIVersionHandler returns a handler which will list the provided versions as available. -func APIVersionHandler(s runtime.NegotiatedSerializer, versions ...string) restful.RouteFunction { +func APIVersionHandler(s runtime.NegotiatedSerializer, getAPIVersionsFunc func(req *restful.Request) *unversioned.APIVersions) restful.RouteFunction { return func(req *restful.Request, resp *restful.Response) { - writeNegotiated(s, unversioned.GroupVersion{}, resp.ResponseWriter, req.Request, http.StatusOK, &unversioned.APIVersions{Versions: versions}) + writeNegotiated(s, unversioned.GroupVersion{}, resp.ResponseWriter, req.Request, http.StatusOK, getAPIVersionsFunc(req)) } } // RootAPIHandler returns a handler which will list the provided groups and versions as available. -func RootAPIHandler(s runtime.NegotiatedSerializer, f func() []unversioned.APIGroup) restful.RouteFunction { +func RootAPIHandler(s runtime.NegotiatedSerializer, f func(req *restful.Request) []unversioned.APIGroup) restful.RouteFunction { return func(req *restful.Request, resp *restful.Response) { - writeNegotiated(s, unversioned.GroupVersion{}, resp.ResponseWriter, req.Request, http.StatusOK, &unversioned.APIGroupList{Groups: f()}) + writeNegotiated(s, unversioned.GroupVersion{}, resp.ResponseWriter, req.Request, http.StatusOK, &unversioned.APIGroupList{Groups: f(req)}) } } diff --git a/pkg/genericapiserver/genericapiserver.go b/pkg/genericapiserver/genericapiserver.go index 8651a8133ea..84ef105fc26 100644 --- a/pkg/genericapiserver/genericapiserver.go +++ b/pkg/genericapiserver/genericapiserver.go @@ -256,9 +256,12 @@ type Config struct { // The range of IPs to be assigned to services with type=ClusterIP or greater ServiceClusterIPRange *net.IPNet - // The IP address for the GenericAPIServer service (must be inside ServiceClusterIPRange + // The IP address for the GenericAPIServer service (must be inside ServiceClusterIPRange) ServiceReadWriteIP net.IP + // Port for the apiserver service. + ServiceReadWritePort int + // The range of ports to be assigned to services with type=NodePort or greater ServiceNodePortRange utilnet.PortRange @@ -308,8 +311,9 @@ type GenericAPIServer struct { ApiGroupVersionOverrides map[string]APIGroupVersionOverride RequestContextMapper api.RequestContextMapper - // External host is the name that should be used in external (public internet) URLs for this GenericAPIServer - externalHost string + // ExternalAddress is the address (hostname or IP and port) that should be used in + // external (public internet) URLs for this GenericAPIServer. + ExternalAddress string // ClusterIP is the IP address of the GenericAPIServer within the cluster. ClusterIP net.IP PublicReadWritePort int @@ -370,6 +374,9 @@ func setDefaults(c *Config) { glog.V(4).Infof("Setting GenericAPIServer service IP to %q (read-write).", serviceReadWriteIP) c.ServiceReadWriteIP = serviceReadWriteIP } + if c.ServiceReadWritePort == 0 { + c.ServiceReadWritePort = 443 + } if c.ServiceNodePortRange.Size == 0 { // TODO: Currently no way to specify an empty range (do we need to allow this?) // We should probably allow this for clouds that don't require NodePort to do load-balancing (GCE) @@ -392,6 +399,13 @@ func setDefaults(c *Config) { if c.RequestContextMapper == nil { c.RequestContextMapper = api.NewRequestContextMapper() } + if len(c.ExternalHost) == 0 && c.PublicAddress != nil { + hostAndPort := c.PublicAddress.String() + if c.ReadWritePort != 0 { + hostAndPort = net.JoinHostPort(hostAndPort, strconv.Itoa(c.ServiceReadWritePort)) + } + c.ExternalHost = hostAndPort + } } // New returns a new instance of GenericAPIServer from the given config. @@ -444,13 +458,12 @@ func New(c *Config) (*GenericAPIServer, error) { cacheTimeout: c.CacheTimeout, MinRequestTimeout: time.Duration(c.MinRequestTimeout) * time.Second, - MasterCount: c.MasterCount, - externalHost: c.ExternalHost, - ClusterIP: c.PublicAddress, - PublicReadWritePort: c.ReadWritePort, - ServiceReadWriteIP: c.ServiceReadWriteIP, - // TODO: ServiceReadWritePort should be passed in as an argument, it may not always be 443 - ServiceReadWritePort: 443, + MasterCount: c.MasterCount, + ExternalAddress: c.ExternalHost, + ClusterIP: c.PublicAddress, + PublicReadWritePort: c.ReadWritePort, + ServiceReadWriteIP: c.ServiceReadWriteIP, + ServiceReadWritePort: c.ServiceReadWritePort, ExtraServicePorts: c.ExtraServicePorts, ExtraEndpointPorts: c.ExtraEndpointPorts, @@ -603,7 +616,7 @@ func (s *GenericAPIServer) InstallAPIGroups(groupsInfo []APIGroupInfo) error { // Installs handler at /apis to list all group versions for discovery func (s *GenericAPIServer) installGroupsDiscoveryHandler() { - apiserver.AddApisWebService(s.Serializer, s.HandlerContainer, s.APIGroupPrefix, func() []unversioned.APIGroup { + apiserver.AddApisWebService(s.Serializer, s.HandlerContainer, s.APIGroupPrefix, func(req *restful.Request) []unversioned.APIGroup { // Return the list of supported groups in sorted order (to have a deterministic order). groups := []unversioned.APIGroup{} groupNames := make([]string, len(s.apiGroupsForDiscovery)) @@ -614,7 +627,10 @@ func (s *GenericAPIServer) installGroupsDiscoveryHandler() { } sort.Strings(groupNames) for _, groupName := range groupNames { - groups = append(groups, s.apiGroupsForDiscovery[groupName]) + apiGroup := s.apiGroupsForDiscovery[groupName] + // Add ServerAddressByClientCIDRs. + apiGroup.ServerAddressByClientCIDRs = s.getServerAddressByClientCIDRs(req.Request) + groups = append(groups, apiGroup) } return groups }) @@ -738,7 +754,13 @@ func (s *GenericAPIServer) installAPIGroup(apiGroupInfo *APIGroupInfo) error { // Install the version handler. if apiGroupInfo.IsLegacyGroup { // Add a handler at /api to enumerate the supported api versions. - apiserver.AddApiWebService(s.Serializer, s.HandlerContainer, apiPrefix, apiVersions) + apiserver.AddApiWebService(s.Serializer, s.HandlerContainer, apiPrefix, func(req *restful.Request) *unversioned.APIVersions { + apiVersionsForDiscovery := unversioned.APIVersions{ + ServerAddressByClientCIDRs: s.getServerAddressByClientCIDRs(req.Request), + Versions: apiVersions, + } + return &apiVersionsForDiscovery + }) } else { // Do not register empty group or empty version. Doing so claims /apis/ for the wrong entity to be returned. // Catching these here places the error much closer to its origin @@ -781,6 +803,27 @@ func (s *GenericAPIServer) RemoveAPIGroupForDiscovery(groupName string) { delete(s.apiGroupsForDiscovery, groupName) } +func (s *GenericAPIServer) getServerAddressByClientCIDRs(req *http.Request) []unversioned.ServerAddressByClientCIDR { + addressCIDRMap := []unversioned.ServerAddressByClientCIDR{ + { + ClientCIDR: "0.0.0.0/0", + + ServerAddress: s.ExternalAddress, + }, + } + + // Add internal CIDR if the request came from internal IP. + clientIP := utilnet.GetClientIP(req) + clusterCIDR := s.ServiceClusterIPRange + if clusterCIDR.Contains(clientIP) { + addressCIDRMap = append(addressCIDRMap, unversioned.ServerAddressByClientCIDR{ + ClientCIDR: clusterCIDR.String(), + ServerAddress: net.JoinHostPort(s.ServiceReadWriteIP.String(), strconv.Itoa(s.ServiceReadWritePort)), + }) + } + return addressCIDRMap +} + func (s *GenericAPIServer) getAPIGroupVersion(apiGroupInfo *APIGroupInfo, groupVersion unversioned.GroupVersion, apiPrefix string) (*apiserver.APIGroupVersion, error) { storage := make(map[string]rest.Storage) for k, v := range apiGroupInfo.VersionedResourcesStorageMap[groupVersion.Version] { @@ -817,17 +860,8 @@ func (s *GenericAPIServer) newAPIGroupVersion(groupMeta apimachinery.GroupMeta, // register their own web services into the Kubernetes mux prior to initialization // of swagger, so that other resource types show up in the documentation. func (s *GenericAPIServer) InstallSwaggerAPI() { - hostAndPort := s.externalHost + hostAndPort := s.ExternalAddress protocol := "https://" - - // TODO: this is kind of messed up, we should just pipe in the full URL from the outside, rather - // than guessing at it. - if len(s.externalHost) == 0 && s.ClusterIP != nil { - host := s.ClusterIP.String() - if s.PublicReadWritePort != 0 { - hostAndPort = net.JoinHostPort(host, strconv.Itoa(s.PublicReadWritePort)) - } - } webServicesUrl := protocol + hostAndPort // Enable swagger UI and discovery API diff --git a/pkg/genericapiserver/genericapiserver_test.go b/pkg/genericapiserver/genericapiserver_test.go index 6b7209a8218..7d121257451 100644 --- a/pkg/genericapiserver/genericapiserver_test.go +++ b/pkg/genericapiserver/genericapiserver_test.go @@ -18,14 +18,22 @@ package genericapiserver import ( "crypto/tls" + "encoding/json" "fmt" + "io/ioutil" "net" "net/http" "net/http/httptest" + "reflect" + "strconv" "testing" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/rest" + "k8s.io/kubernetes/pkg/api/testapi" + "k8s.io/kubernetes/pkg/api/unversioned" + apiutil "k8s.io/kubernetes/pkg/api/util" + "k8s.io/kubernetes/pkg/apimachinery/registered" "k8s.io/kubernetes/pkg/apis/extensions" "k8s.io/kubernetes/pkg/apiserver" @@ -46,20 +54,27 @@ func setUp(t *testing.T) (GenericAPIServer, *etcdtesting.EtcdTestServer, Config, return genericapiserver, etcdServer, config, assert.New(t) } -// TestNew verifies that the New function returns a GenericAPIServer -// using the configuration properly. -func TestNew(t *testing.T) { +func newMaster(t *testing.T) (*GenericAPIServer, *etcdtesting.EtcdTestServer, Config, *assert.Assertions) { _, etcdserver, config, assert := setUp(t) - defer etcdserver.Terminate(t) config.ProxyDialer = func(network, addr string) (net.Conn, error) { return nil, nil } config.ProxyTLSClientConfig = &tls.Config{} config.Serializer = api.Codecs + config.APIPrefix = "/api" + config.APIGroupPrefix = "/apis" s, err := New(&config) if err != nil { t.Fatalf("Error in bringing up the server: %v", err) } + return s, etcdserver, config, assert +} + +// TestNew verifies that the New function returns a GenericAPIServer +// using the configuration properly. +func TestNew(t *testing.T) { + s, etcdserver, config, assert := newMaster(t) + defer etcdserver.Terminate(t) // Verify many of the variables match their config counterparts assert.Equal(s.enableLogsSupport, config.EnableLogsSupport) @@ -75,7 +90,7 @@ func TestNew(t *testing.T) { assert.Equal(s.ApiGroupVersionOverrides, config.APIGroupVersionOverrides) assert.Equal(s.RequestContextMapper, config.RequestContextMapper) assert.Equal(s.cacheTimeout, config.CacheTimeout) - assert.Equal(s.externalHost, config.ExternalHost) + assert.Equal(s.ExternalAddress, config.ExternalHost) assert.Equal(s.ClusterIP, config.PublicAddress) assert.Equal(s.PublicReadWritePort, config.ReadWritePort) assert.Equal(s.ServiceReadWriteIP, config.ServiceReadWriteIP) @@ -211,7 +226,7 @@ func TestInstallSwaggerAPI(t *testing.T) { // Empty externalHost verification mux = http.NewServeMux() server.HandlerContainer = NewHandlerContainer(mux, nil) - server.externalHost = "" + server.ExternalAddress = "" server.ClusterIP = net.IPv4(10, 10, 10, 10) server.PublicReadWritePort = 1010 server.InstallSwaggerAPI() @@ -219,3 +234,170 @@ func TestInstallSwaggerAPI(t *testing.T) { assert.Equal("/swaggerapi/", ws[0].RootPath(), "SwaggerAPI did not install to the proper path. %s != /swaggerapi", ws[0].RootPath()) } } + +func decodeResponse(resp *http.Response, obj interface{}) error { + defer resp.Body.Close() + + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + if err := json.Unmarshal(data, obj); err != nil { + return err + } + return nil +} + +func getGroupList(server *httptest.Server) (*unversioned.APIGroupList, error) { + resp, err := http.Get(server.URL + "/apis") + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected server response, expected %d, actual: %d", http.StatusOK, resp.StatusCode) + } + + groupList := unversioned.APIGroupList{} + err = decodeResponse(resp, &groupList) + return &groupList, err +} + +func TestDiscoveryAtAPIS(t *testing.T) { + master, etcdserver, config, assert := newMaster(t) + defer etcdserver.Terminate(t) + + server := httptest.NewServer(master.HandlerContainer.ServeMux) + groupList, err := getGroupList(server) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + assert.Equal(0, len(groupList.Groups)) + + // Add a Group. + extensionsGroupName := extensions.GroupName + extensionsVersions := []unversioned.GroupVersionForDiscovery{ + { + GroupVersion: testapi.Extensions.GroupVersion().String(), + Version: testapi.Extensions.GroupVersion().Version, + }, + } + extensionsPreferredVersion := unversioned.GroupVersionForDiscovery{ + GroupVersion: config.StorageVersions[extensions.GroupName], + Version: apiutil.GetVersion(config.StorageVersions[extensions.GroupName]), + } + master.AddAPIGroupForDiscovery(unversioned.APIGroup{ + Name: extensionsGroupName, + Versions: extensionsVersions, + PreferredVersion: extensionsPreferredVersion, + }) + + groupList, err = getGroupList(server) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assert.Equal(1, len(groupList.Groups)) + groupListGroup := groupList.Groups[0] + assert.Equal(extensionsGroupName, groupListGroup.Name) + assert.Equal(extensionsVersions, groupListGroup.Versions) + assert.Equal(extensionsPreferredVersion, groupListGroup.PreferredVersion) + assert.Equal(master.getServerAddressByClientCIDRs(&http.Request{}), groupListGroup.ServerAddressByClientCIDRs) + + // Remove the group. + master.RemoveAPIGroupForDiscovery(extensionsGroupName) + groupList, err = getGroupList(server) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + assert.Equal(0, len(groupList.Groups)) +} + +func TestGetServerAddressByClientCIDRs(t *testing.T) { + s, etcdserver, _, _ := newMaster(t) + defer etcdserver.Terminate(t) + + publicAddressCIDRMap := []unversioned.ServerAddressByClientCIDR{ + { + ClientCIDR: "0.0.0.0/0", + + ServerAddress: s.ExternalAddress, + }, + } + internalAddressCIDRMap := []unversioned.ServerAddressByClientCIDR{ + publicAddressCIDRMap[0], + { + ClientCIDR: s.ServiceClusterIPRange.String(), + ServerAddress: net.JoinHostPort(s.ServiceReadWriteIP.String(), strconv.Itoa(s.ServiceReadWritePort)), + }, + } + internalIP := "10.0.0.1" + publicIP := "1.1.1.1" + testCases := []struct { + Request http.Request + ExpectedMap []unversioned.ServerAddressByClientCIDR + }{ + { + Request: http.Request{}, + ExpectedMap: publicAddressCIDRMap, + }, + { + Request: http.Request{ + Header: map[string][]string{ + "X-Real-Ip": {internalIP}, + }, + }, + ExpectedMap: internalAddressCIDRMap, + }, + { + Request: http.Request{ + Header: map[string][]string{ + "X-Real-Ip": {publicIP}, + }, + }, + ExpectedMap: publicAddressCIDRMap, + }, + { + Request: http.Request{ + Header: map[string][]string{ + "X-Forwarded-For": {internalIP}, + }, + }, + ExpectedMap: internalAddressCIDRMap, + }, + { + Request: http.Request{ + Header: map[string][]string{ + "X-Forwarded-For": {publicIP}, + }, + }, + ExpectedMap: publicAddressCIDRMap, + }, + + { + Request: http.Request{ + RemoteAddr: internalIP, + }, + ExpectedMap: internalAddressCIDRMap, + }, + { + Request: http.Request{ + RemoteAddr: publicIP, + }, + ExpectedMap: publicAddressCIDRMap, + }, + { + Request: http.Request{ + RemoteAddr: "invalidIP", + }, + ExpectedMap: publicAddressCIDRMap, + }, + } + + for i, test := range testCases { + if a, e := s.getServerAddressByClientCIDRs(&test.Request), test.ExpectedMap; reflect.DeepEqual(e, a) != true { + t.Fatalf("test case %d failed. expected: %v, actual: %v", i+1, e, a) + } + } +} diff --git a/pkg/master/master_test.go b/pkg/master/master_test.go index 03f44567e99..f108c75d48d 100644 --- a/pkg/master/master_test.go +++ b/pkg/master/master_test.go @@ -304,6 +304,7 @@ func TestDiscoveryAtAPIS(t *testing.T) { }, } + assert.Equal(2, len(groupList.Groups)) assert.Equal(expectGroupNames[0], groupList.Groups[0].Name) assert.Equal(expectGroupNames[1], groupList.Groups[1].Name) @@ -383,6 +384,7 @@ func initThirdParty(t *testing.T, version string) (*Master, *etcdtesting.EtcdTes }, } master.thirdPartyStorage = etcdstorage.NewEtcdStorage(etcdserver.Client, testapi.Extensions.Codec(), etcdtest.PathPrefix(), false) + _, master.ServiceClusterIPRange, _ = net.ParseCIDR("10.0.0.0/24") if !assert.NoError(master.InstallThirdPartyResource(api)) { t.FailNow() diff --git a/pkg/util/net/http.go b/pkg/util/net/http.go index 1c618accf8c..f3d4473f0a8 100644 --- a/pkg/util/net/http.go +++ b/pkg/util/net/http.go @@ -120,3 +120,36 @@ func GetHTTPClient(req *http.Request) string { } return "unknown" } + +// Extracts and returns the clients IP from the given request. +// Looks at X-Forwarded-For header, X-Real-Ip header and request.RemoteAddr in that order. +// Returns nil if none of them are set or is set to an invalid value. +func GetClientIP(req *http.Request) net.IP { + hdr := req.Header + // First check the X-Forwarded-For header for requests via proxy. + hdrForwardedFor := hdr.Get("X-Forwarded-For") + if hdrForwardedFor != "" { + // X-Forwarded-For can be a csv of IPs in case of multiple proxies. + // Use the first valid one. + parts := strings.Split(hdrForwardedFor, ",") + for _, part := range parts { + ip := net.ParseIP(strings.TrimSpace(part)) + if ip != nil { + return ip + } + } + } + + // Try the X-Real-Ip header. + hdrRealIp := hdr.Get("X-Real-Ip") + if hdrRealIp != "" { + ip := net.ParseIP(hdrRealIp) + if ip != nil { + return ip + } + } + + // Fallback to Remote Address in request, which will give the correct client IP when there is no proxy. + ip := net.ParseIP(req.RemoteAddr) + return ip +} diff --git a/pkg/util/net/http_test.go b/pkg/util/net/http_test.go new file mode 100644 index 00000000000..7990a51d115 --- /dev/null +++ b/pkg/util/net/http_test.go @@ -0,0 +1,102 @@ +/* +Copyright 2016 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package net + +import ( + "net" + "net/http" + "reflect" + "testing" +) + +func TestGetClientIP(t *testing.T) { + ipString := "10.0.0.1" + ip := net.ParseIP(ipString) + invalidIPString := "invalidIPString" + testCases := []struct { + Request http.Request + ExpectedIP net.IP + }{ + { + Request: http.Request{}, + }, + { + Request: http.Request{ + Header: map[string][]string{ + "X-Real-Ip": {ipString}, + }, + }, + ExpectedIP: ip, + }, + { + Request: http.Request{ + Header: map[string][]string{ + "X-Real-Ip": {invalidIPString}, + }, + }, + }, + { + Request: http.Request{ + Header: map[string][]string{ + "X-Forwarded-For": {ipString}, + }, + }, + ExpectedIP: ip, + }, + { + Request: http.Request{ + Header: map[string][]string{ + "X-Forwarded-For": {invalidIPString}, + }, + }, + }, + { + Request: http.Request{ + Header: map[string][]string{ + "X-Forwarded-For": {invalidIPString + "," + ipString}, + }, + }, + ExpectedIP: ip, + }, + { + Request: http.Request{ + RemoteAddr: ipString, + }, + ExpectedIP: ip, + }, + { + Request: http.Request{ + RemoteAddr: invalidIPString, + }, + }, + { + Request: http.Request{ + Header: map[string][]string{ + "X-Forwarded-For": {invalidIPString}, + }, + RemoteAddr: ipString, + }, + ExpectedIP: ip, + }, + } + + for i, test := range testCases { + if a, e := GetClientIP(&test.Request), test.ExpectedIP; reflect.DeepEqual(e, a) != true { + t.Fatalf("test case %d failed. expected: %v, actual: %v", i+1, e, a) + } + } +}