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