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	v1 "k8s.io/api/core/v1"
25	"k8s.io/apimachinery/pkg/api/resource"
26	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
27	"k8s.io/apimachinery/pkg/runtime"
28	"k8s.io/apimachinery/pkg/util/validation"
29	cmdutil "k8s.io/kubectl/pkg/cmd/util"
30	"k8s.io/kubectl/pkg/generate"
31)
32
33// getLabels returns map of labels.
34func getLabels(params map[string]string, name string) (map[string]string, error) {
35	labelString, found := params["labels"]
36	var labels map[string]string
37	var err error
38	if found && len(labelString) > 0 {
39		labels, err = generate.ParseLabels(labelString)
40		if err != nil {
41			return nil, err
42		}
43	} else {
44		labels = map[string]string{
45			"run": name,
46		}
47	}
48	return labels, nil
49}
50
51// getName returns the name of newly created resource.
52func getName(params map[string]string) (string, error) {
53	name, found := params["name"]
54	if !found || len(name) == 0 {
55		name, found = params["default-name"]
56		if !found || len(name) == 0 {
57			return "", fmt.Errorf("'name' is a required parameter")
58		}
59	}
60	return name, nil
61}
62
63// getParams returns map of generic parameters.
64func getParams(genericParams map[string]interface{}) (map[string]string, error) {
65	params := map[string]string{}
66	for key, value := range genericParams {
67		strVal, isString := value.(string)
68		if !isString {
69			return nil, fmt.Errorf("expected string, saw %v for '%s'", value, key)
70		}
71		params[key] = strVal
72	}
73	return params, nil
74}
75
76// getArgs returns arguments for the container command.
77func getArgs(genericParams map[string]interface{}) ([]string, error) {
78	args := []string{}
79	val, found := genericParams["args"]
80	if found {
81		var isArray bool
82		args, isArray = val.([]string)
83		if !isArray {
84			return nil, fmt.Errorf("expected []string, found: %v", val)
85		}
86		delete(genericParams, "args")
87	}
88	return args, nil
89}
90
91// getAnnotations returns map of annotations.
92func getAnnotations(genericParams map[string]interface{}) (map[string]string, error) {
93	annotationStrings, ok := genericParams["annotations"]
94	if !ok {
95		return nil, nil
96	}
97
98	annotationStringArray, ok := annotationStrings.([]string)
99	if !ok {
100		return nil, fmt.Errorf("expected []string, found: %v", annotationStrings)
101	}
102
103	annotations, _, err := cmdutil.ParsePairs(annotationStringArray, "annotations", false)
104	if err != nil {
105		return nil, err
106	}
107
108	delete(genericParams, "annotations")
109	return annotations, nil
110}
111
112// getEnvs returns environment variables.
113func getEnvs(genericParams map[string]interface{}) ([]v1.EnvVar, error) {
114	var envs []v1.EnvVar
115	envStrings, found := genericParams["env"]
116	if found {
117		if envStringArray, isArray := envStrings.([]string); isArray {
118			var err error
119			envs, err = parseEnvs(envStringArray)
120			if err != nil {
121				return nil, err
122			}
123			delete(genericParams, "env")
124		} else {
125			return nil, fmt.Errorf("expected []string, found: %v", envStrings)
126		}
127	}
128	return envs, nil
129}
130
131// populateResourceListV1 takes strings of form <resourceName1>=<value1>,<resourceName1>=<value2>
132// and returns ResourceList.
133func populateResourceListV1(spec string) (v1.ResourceList, error) {
134	// empty input gets a nil response to preserve generator test expected behaviors
135	if spec == "" {
136		return nil, nil
137	}
138
139	result := v1.ResourceList{}
140	resourceStatements := strings.Split(spec, ",")
141	for _, resourceStatement := range resourceStatements {
142		parts := strings.Split(resourceStatement, "=")
143		if len(parts) != 2 {
144			return nil, fmt.Errorf("Invalid argument syntax %v, expected <resource>=<value>", resourceStatement)
145		}
146		resourceName := v1.ResourceName(parts[0])
147		resourceQuantity, err := resource.ParseQuantity(parts[1])
148		if err != nil {
149			return nil, err
150		}
151		result[resourceName] = resourceQuantity
152	}
153	return result, nil
154}
155
156// HandleResourceRequirementsV1 parses the limits and requests parameters if specified
157// and returns ResourceRequirements.
158func HandleResourceRequirementsV1(params map[string]string) (v1.ResourceRequirements, error) {
159	result := v1.ResourceRequirements{}
160	limits, err := populateResourceListV1(params["limits"])
161	if err != nil {
162		return result, err
163	}
164	result.Limits = limits
165	requests, err := populateResourceListV1(params["requests"])
166	if err != nil {
167		return result, err
168	}
169	result.Requests = requests
170	return result, nil
171}
172
173// updatePodContainers updates PodSpec.Containers with passed parameters.
174func updatePodContainers(params map[string]string, args []string, envs []v1.EnvVar, imagePullPolicy v1.PullPolicy, podSpec *v1.PodSpec) error {
175	if len(args) > 0 {
176		command, err := generate.GetBool(params, "command", false)
177		if err != nil {
178			return err
179		}
180		if command {
181			podSpec.Containers[0].Command = args
182		} else {
183			podSpec.Containers[0].Args = args
184		}
185	}
186
187	if len(envs) > 0 {
188		podSpec.Containers[0].Env = envs
189	}
190
191	if len(imagePullPolicy) > 0 {
192		// imagePullPolicy should be valid here since we have verified it before.
193		podSpec.Containers[0].ImagePullPolicy = imagePullPolicy
194	}
195	return nil
196}
197
198// updatePodContainers updates PodSpec.Containers.Ports with passed parameters.
199func updatePodPorts(params map[string]string, podSpec *v1.PodSpec) (err error) {
200	port := -1
201	hostPort := -1
202	if len(params["port"]) > 0 {
203		port, err = strconv.Atoi(params["port"])
204		if err != nil {
205			return err
206		}
207	}
208
209	if len(params["hostport"]) > 0 {
210		hostPort, err = strconv.Atoi(params["hostport"])
211		if err != nil {
212			return err
213		}
214		if hostPort > 0 && port < 0 {
215			return fmt.Errorf("--hostport requires --port to be specified")
216		}
217	}
218
219	// Don't include the port if it was not specified.
220	if len(params["port"]) > 0 {
221		podSpec.Containers[0].Ports = []v1.ContainerPort{
222			{
223				ContainerPort: int32(port),
224			},
225		}
226		if hostPort > 0 {
227			podSpec.Containers[0].Ports[0].HostPort = int32(hostPort)
228		}
229	}
230	return nil
231}
232
233type BasicPod struct{}
234
235func (BasicPod) ParamNames() []generate.GeneratorParam {
236	return []generate.GeneratorParam{
237		{Name: "labels", Required: false},
238		{Name: "annotations", Required: false},
239		{Name: "default-name", Required: false},
240		{Name: "name", Required: true},
241		{Name: "image", Required: true},
242		{Name: "image-pull-policy", Required: false},
243		{Name: "port", Required: false},
244		{Name: "hostport", Required: false},
245		{Name: "stdin", Required: false},
246		{Name: "leave-stdin-open", Required: false},
247		{Name: "tty", Required: false},
248		{Name: "restart", Required: false},
249		{Name: "command", Required: false},
250		{Name: "args", Required: false},
251		{Name: "env", Required: false},
252		{Name: "requests", Required: false},
253		{Name: "limits", Required: false},
254		{Name: "serviceaccount", Required: false},
255		{Name: "privileged", Required: false},
256	}
257}
258
259func (BasicPod) Generate(genericParams map[string]interface{}) (runtime.Object, error) {
260	args, err := getArgs(genericParams)
261	if err != nil {
262		return nil, err
263	}
264
265	envs, err := getEnvs(genericParams)
266	if err != nil {
267		return nil, err
268	}
269
270	annotations, err := getAnnotations(genericParams)
271	if err != nil {
272		return nil, err
273	}
274
275	params, err := getParams(genericParams)
276	if err != nil {
277		return nil, err
278	}
279
280	name, err := getName(params)
281	if err != nil {
282		return nil, err
283	}
284
285	labels, err := getLabels(params, name)
286	if err != nil {
287		return nil, err
288	}
289
290	stdin, err := generate.GetBool(params, "stdin", false)
291	if err != nil {
292		return nil, err
293	}
294	leaveStdinOpen, err := generate.GetBool(params, "leave-stdin-open", false)
295	if err != nil {
296		return nil, err
297	}
298
299	tty, err := generate.GetBool(params, "tty", false)
300	if err != nil {
301		return nil, err
302	}
303
304	resourceRequirements, err := HandleResourceRequirementsV1(params)
305	if err != nil {
306		return nil, err
307	}
308
309	restartPolicy := v1.RestartPolicy(params["restart"])
310	if len(restartPolicy) == 0 {
311		restartPolicy = v1.RestartPolicyAlways
312	}
313
314	privileged, err := generate.GetBool(params, "privileged", false)
315	if err != nil {
316		return nil, err
317	}
318	var securityContext *v1.SecurityContext
319	if privileged {
320		securityContext = &v1.SecurityContext{
321			Privileged: &privileged,
322		}
323	}
324
325	pod := v1.Pod{
326		ObjectMeta: metav1.ObjectMeta{
327			Name:        name,
328			Labels:      labels,
329			Annotations: annotations,
330		},
331		Spec: v1.PodSpec{
332			ServiceAccountName: params["serviceaccount"],
333			Containers: []v1.Container{
334				{
335					Name:            name,
336					Image:           params["image"],
337					Stdin:           stdin,
338					StdinOnce:       !leaveStdinOpen && stdin,
339					TTY:             tty,
340					Resources:       resourceRequirements,
341					SecurityContext: securityContext,
342				},
343			},
344			DNSPolicy:     v1.DNSClusterFirst,
345			RestartPolicy: restartPolicy,
346		},
347	}
348	imagePullPolicy := v1.PullPolicy(params["image-pull-policy"])
349	if err = updatePodContainers(params, args, envs, imagePullPolicy, &pod.Spec); err != nil {
350		return nil, err
351	}
352
353	if err := updatePodPorts(params, &pod.Spec); err != nil {
354		return nil, err
355	}
356	return &pod, nil
357}
358
359// parseEnvs converts string into EnvVar objects.
360func parseEnvs(envArray []string) ([]v1.EnvVar, error) {
361	envs := make([]v1.EnvVar, 0, len(envArray))
362	for _, env := range envArray {
363		pos := strings.Index(env, "=")
364		if pos == -1 {
365			return nil, fmt.Errorf("invalid env: %v", env)
366		}
367		name := env[:pos]
368		value := env[pos+1:]
369		if len(name) == 0 {
370			return nil, fmt.Errorf("invalid env: %v", env)
371		}
372		if len(validation.IsEnvVarName(name)) != 0 {
373			return nil, fmt.Errorf("invalid env: %v", env)
374		}
375		envVar := v1.EnvVar{Name: name, Value: value}
376		envs = append(envs, envVar)
377	}
378	return envs, nil
379}
380