1/* 2Copyright 2014 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package versioned 18 19import ( 20 "fmt" 21 "strconv" 22 "strings" 23 24 "k8s.io/api/core/v1" 25 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 "k8s.io/apimachinery/pkg/runtime" 27 "k8s.io/apimachinery/pkg/util/intstr" 28 "k8s.io/kubectl/pkg/generate" 29) 30 31// The only difference between ServiceGeneratorV1 and V2 is that the service port is named "default" in V1, while it is left unnamed in V2. 32type ServiceGeneratorV1 struct{} 33 34func (ServiceGeneratorV1) ParamNames() []generate.GeneratorParam { 35 return paramNames() 36} 37 38func (ServiceGeneratorV1) Generate(params map[string]interface{}) (runtime.Object, error) { 39 params["port-name"] = "default" 40 return generateService(params) 41} 42 43type ServiceGeneratorV2 struct{} 44 45func (ServiceGeneratorV2) ParamNames() []generate.GeneratorParam { 46 return paramNames() 47} 48 49func (ServiceGeneratorV2) Generate(params map[string]interface{}) (runtime.Object, error) { 50 return generateService(params) 51} 52 53func paramNames() []generate.GeneratorParam { 54 return []generate.GeneratorParam{ 55 {Name: "default-name", Required: true}, 56 {Name: "name", Required: false}, 57 {Name: "selector", Required: true}, 58 // port will be used if a user specifies --port OR the exposed object 59 // has one port 60 {Name: "port", Required: false}, 61 // ports will be used iff a user doesn't specify --port AND the 62 // exposed object has multiple ports 63 {Name: "ports", Required: false}, 64 {Name: "labels", Required: false}, 65 {Name: "external-ip", Required: false}, 66 {Name: "load-balancer-ip", Required: false}, 67 {Name: "type", Required: false}, 68 {Name: "protocol", Required: false}, 69 // protocols will be used to keep port-protocol mapping derived from 70 // exposed object 71 {Name: "protocols", Required: false}, 72 {Name: "container-port", Required: false}, // alias of target-port 73 {Name: "target-port", Required: false}, 74 {Name: "port-name", Required: false}, 75 {Name: "session-affinity", Required: false}, 76 {Name: "cluster-ip", Required: false}, 77 } 78} 79 80func generateService(genericParams map[string]interface{}) (runtime.Object, error) { 81 params := map[string]string{} 82 for key, value := range genericParams { 83 strVal, isString := value.(string) 84 if !isString { 85 return nil, fmt.Errorf("expected string, saw %v for '%s'", value, key) 86 } 87 params[key] = strVal 88 } 89 selectorString, found := params["selector"] 90 if !found || len(selectorString) == 0 { 91 return nil, fmt.Errorf("'selector' is a required parameter") 92 } 93 selector, err := generate.ParseLabels(selectorString) 94 if err != nil { 95 return nil, err 96 } 97 98 labelsString, found := params["labels"] 99 var labels map[string]string 100 if found && len(labelsString) > 0 { 101 labels, err = generate.ParseLabels(labelsString) 102 if err != nil { 103 return nil, err 104 } 105 } 106 107 name, found := params["name"] 108 if !found || len(name) == 0 { 109 name, found = params["default-name"] 110 if !found || len(name) == 0 { 111 return nil, fmt.Errorf("'name' is a required parameter") 112 } 113 } 114 115 isHeadlessService := params["cluster-ip"] == "None" 116 117 ports := []v1.ServicePort{} 118 servicePortName, found := params["port-name"] 119 if !found { 120 // Leave the port unnamed. 121 servicePortName = "" 122 } 123 124 protocolsString, found := params["protocols"] 125 var portProtocolMap map[string]string 126 if found && len(protocolsString) > 0 { 127 portProtocolMap, err = generate.ParseProtocols(protocolsString) 128 if err != nil { 129 return nil, err 130 } 131 } 132 // ports takes precedence over port since it will be 133 // specified only when the user hasn't specified a port 134 // via --port and the exposed object has multiple ports. 135 var portString string 136 if portString, found = params["ports"]; !found { 137 portString, found = params["port"] 138 if !found && !isHeadlessService { 139 return nil, fmt.Errorf("'ports' or 'port' is a required parameter") 140 } 141 } 142 143 if portString != "" { 144 portStringSlice := strings.Split(portString, ",") 145 for i, stillPortString := range portStringSlice { 146 port, err := strconv.Atoi(stillPortString) 147 if err != nil { 148 return nil, err 149 } 150 name := servicePortName 151 // If we are going to assign multiple ports to a service, we need to 152 // generate a different name for each one. 153 if len(portStringSlice) > 1 { 154 name = fmt.Sprintf("port-%d", i+1) 155 } 156 protocol := params["protocol"] 157 158 switch { 159 case len(protocol) == 0 && len(portProtocolMap) == 0: 160 // Default to TCP, what the flag was doing previously. 161 protocol = "TCP" 162 case len(protocol) > 0 && len(portProtocolMap) > 0: 163 // User has specified the --protocol while exposing a multiprotocol resource 164 // We should stomp multiple protocols with the one specified ie. do nothing 165 case len(protocol) == 0 && len(portProtocolMap) > 0: 166 // no --protocol and we expose a multiprotocol resource 167 protocol = "TCP" // have the default so we can stay sane 168 if exposeProtocol, found := portProtocolMap[stillPortString]; found { 169 protocol = exposeProtocol 170 } 171 } 172 ports = append(ports, v1.ServicePort{ 173 Name: name, 174 Port: int32(port), 175 Protocol: v1.Protocol(protocol), 176 }) 177 } 178 } 179 180 service := v1.Service{ 181 ObjectMeta: metav1.ObjectMeta{ 182 Name: name, 183 Labels: labels, 184 }, 185 Spec: v1.ServiceSpec{ 186 Selector: selector, 187 Ports: ports, 188 }, 189 } 190 targetPortString := params["target-port"] 191 if len(targetPortString) == 0 { 192 targetPortString = params["container-port"] 193 } 194 if len(targetPortString) > 0 { 195 var targetPort intstr.IntOrString 196 if portNum, err := strconv.Atoi(targetPortString); err != nil { 197 targetPort = intstr.FromString(targetPortString) 198 } else { 199 targetPort = intstr.FromInt(portNum) 200 } 201 // Use the same target-port for every port 202 for i := range service.Spec.Ports { 203 service.Spec.Ports[i].TargetPort = targetPort 204 } 205 } else { 206 // If --target-port or --container-port haven't been specified, this 207 // should be the same as Port 208 for i := range service.Spec.Ports { 209 port := service.Spec.Ports[i].Port 210 service.Spec.Ports[i].TargetPort = intstr.FromInt(int(port)) 211 } 212 } 213 if len(params["external-ip"]) > 0 { 214 service.Spec.ExternalIPs = []string{params["external-ip"]} 215 } 216 if len(params["type"]) != 0 { 217 service.Spec.Type = v1.ServiceType(params["type"]) 218 } 219 if service.Spec.Type == v1.ServiceTypeLoadBalancer { 220 service.Spec.LoadBalancerIP = params["load-balancer-ip"] 221 } 222 if len(params["session-affinity"]) != 0 { 223 switch v1.ServiceAffinity(params["session-affinity"]) { 224 case v1.ServiceAffinityNone: 225 service.Spec.SessionAffinity = v1.ServiceAffinityNone 226 case v1.ServiceAffinityClientIP: 227 service.Spec.SessionAffinity = v1.ServiceAffinityClientIP 228 default: 229 return nil, fmt.Errorf("unknown session affinity: %s", params["session-affinity"]) 230 } 231 } 232 if len(params["cluster-ip"]) != 0 { 233 if params["cluster-ip"] == "None" { 234 service.Spec.ClusterIP = v1.ClusterIPNone 235 } else { 236 service.Spec.ClusterIP = params["cluster-ip"] 237 } 238 } 239 return &service, nil 240} 241