From 29453805d1390a8cae30c6383dfd53d18bc4dac3 Mon Sep 17 00:00:00 2001 From: Sean Sullivan Date: Wed, 24 Jul 2019 11:59:28 -0700 Subject: [PATCH] Move pkg/kubectl/util to staging Kubernetes-commit: 7083332c634d540c2769b48e32fc4afb8f4f6cd7 --- pkg/util/pod_port.go | 36 +++++ pkg/util/pod_port_test.go | 98 +++++++++++++ pkg/util/service_port.go | 59 ++++++++ pkg/util/service_port_test.go | 266 ++++++++++++++++++++++++++++++++++ pkg/util/umask.go | 28 ++++ pkg/util/umask_windows.go | 28 ++++ pkg/util/util.go | 93 ++++++++++++ pkg/util/util_test.go | 202 ++++++++++++++++++++++++++ 8 files changed, 810 insertions(+) create mode 100644 pkg/util/pod_port.go create mode 100644 pkg/util/pod_port_test.go create mode 100644 pkg/util/service_port.go create mode 100644 pkg/util/service_port_test.go create mode 100644 pkg/util/umask.go create mode 100644 pkg/util/umask_windows.go create mode 100644 pkg/util/util.go create mode 100644 pkg/util/util_test.go diff --git a/pkg/util/pod_port.go b/pkg/util/pod_port.go new file mode 100644 index 00000000..6d78501a --- /dev/null +++ b/pkg/util/pod_port.go @@ -0,0 +1,36 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 util + +import ( + "fmt" + + "k8s.io/api/core/v1" +) + +// LookupContainerPortNumberByName find containerPort number by its named port name +func LookupContainerPortNumberByName(pod v1.Pod, name string) (int32, error) { + for _, ctr := range pod.Spec.Containers { + for _, ctrportspec := range ctr.Ports { + if ctrportspec.Name == name { + return ctrportspec.ContainerPort, nil + } + } + } + + return int32(-1), fmt.Errorf("Pod '%s' does not have a named port '%s'", pod.Name, name) +} diff --git a/pkg/util/pod_port_test.go b/pkg/util/pod_port_test.go new file mode 100644 index 00000000..cecf5151 --- /dev/null +++ b/pkg/util/pod_port_test.go @@ -0,0 +1,98 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 util + +import ( + "testing" + + "k8s.io/api/core/v1" +) + +func TestLookupContainerPortNumberByName(t *testing.T) { + tests := []struct { + name string + pod v1.Pod + portname string + portnum int32 + err bool + }{ + { + name: "test success 1", + pod: v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Ports: []v1.ContainerPort{ + { + Name: "https", + ContainerPort: int32(443)}, + { + Name: "http", + ContainerPort: int32(80)}, + }, + }, + }, + }, + }, + portname: "http", + portnum: int32(80), + err: false, + }, + { + name: "test faulure 1", + pod: v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Ports: []v1.ContainerPort{ + { + Name: "https", + ContainerPort: int32(443)}, + }, + }, + }, + }, + }, + portname: "www", + portnum: int32(0), + err: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + portnum, err := LookupContainerPortNumberByName(tt.pod, tt.portname) + if err != nil { + if tt.err { + return + } + + t.Errorf("%v: unexpected error: %v", tt.name, err) + return + } + + if tt.err { + t.Errorf("%v: unexpected success", tt.name) + return + } + + if portnum != tt.portnum { + t.Errorf("%v: expected port number %v; got %v", tt.name, tt.portnum, portnum) + } + }) + } +} diff --git a/pkg/util/service_port.go b/pkg/util/service_port.go new file mode 100644 index 00000000..bc56ab7d --- /dev/null +++ b/pkg/util/service_port.go @@ -0,0 +1,59 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 util + +import ( + "fmt" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// LookupContainerPortNumberByServicePort implements +// the handling of resolving container named port, as well as ignoring targetPort when clusterIP=None +// It returns an error when a named port can't find a match (with -1 returned), or when the service does not +// declare such port (with the input port number returned). +func LookupContainerPortNumberByServicePort(svc v1.Service, pod v1.Pod, port int32) (int32, error) { + for _, svcportspec := range svc.Spec.Ports { + if svcportspec.Port != port { + continue + } + if svc.Spec.ClusterIP == v1.ClusterIPNone { + return port, nil + } + if svcportspec.TargetPort.Type == intstr.Int { + if svcportspec.TargetPort.IntValue() == 0 { + // targetPort is omitted, and the IntValue() would be zero + return svcportspec.Port, nil + } + return int32(svcportspec.TargetPort.IntValue()), nil + } + return LookupContainerPortNumberByName(pod, svcportspec.TargetPort.String()) + } + return port, fmt.Errorf("Service %s does not have a service port %d", svc.Name, port) +} + +// LookupServicePortNumberByName find service port number by its named port name +func LookupServicePortNumberByName(svc v1.Service, name string) (int32, error) { + for _, svcportspec := range svc.Spec.Ports { + if svcportspec.Name == name { + return svcportspec.Port, nil + } + } + + return int32(-1), fmt.Errorf("Service '%s' does not have a named port '%s'", svc.Name, name) +} diff --git a/pkg/util/service_port_test.go b/pkg/util/service_port_test.go new file mode 100644 index 00000000..f15ae3c3 --- /dev/null +++ b/pkg/util/service_port_test.go @@ -0,0 +1,266 @@ +/* +Copyright 2018 The Kubernetes Authors. + +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 util + +import ( + "testing" + + "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestLookupContainerPortNumberByServicePort(t *testing.T) { + tests := []struct { + name string + svc v1.Service + pod v1.Pod + port int32 + containerPort int32 + err bool + }{ + { + name: "test success 1 (int port)", + svc: v1.Service{ + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + }, + }, + }, + pod: v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Ports: []v1.ContainerPort{ + { + Name: "http", + ContainerPort: int32(8080)}, + }, + }, + }, + }, + }, + port: 80, + containerPort: 8080, + err: false, + }, + { + name: "test success 2 (clusterIP: None)", + svc: v1.Service{ + Spec: v1.ServiceSpec{ + ClusterIP: v1.ClusterIPNone, + Ports: []v1.ServicePort{ + { + Port: 80, + TargetPort: intstr.FromInt(8080), + }, + }, + }, + }, + pod: v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Ports: []v1.ContainerPort{ + { + Name: "http", + ContainerPort: int32(8080)}, + }, + }, + }, + }, + }, + port: 80, + containerPort: 80, + err: false, + }, + { + name: "test success 3 (named port)", + svc: v1.Service{ + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Port: 80, + TargetPort: intstr.FromString("http"), + }, + }, + }, + }, + pod: v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Ports: []v1.ContainerPort{ + { + Name: "http", + ContainerPort: int32(8080)}, + }, + }, + }, + }, + }, + port: 80, + containerPort: 8080, + err: false, + }, + { + name: "test success (targetPort omitted)", + svc: v1.Service{ + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Port: 80, + }, + }, + }, + }, + pod: v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Ports: []v1.ContainerPort{ + { + Name: "http", + ContainerPort: int32(80)}, + }, + }, + }, + }, + }, + port: 80, + containerPort: 80, + err: false, + }, + { + name: "test failure 1 (cannot find a matching named port)", + svc: v1.Service{ + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Port: 80, + TargetPort: intstr.FromString("http"), + }, + }, + }, + }, + pod: v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Ports: []v1.ContainerPort{ + { + Name: "https", + ContainerPort: int32(443)}, + }, + }, + }, + }, + }, + port: 80, + containerPort: -1, + err: true, + }, + { + name: "test failure 2 (cannot find a matching service port)", + svc: v1.Service{ + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Port: 80, + TargetPort: intstr.FromString("http"), + }, + }, + }, + }, + pod: v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Ports: []v1.ContainerPort{ + { + Name: "https", + ContainerPort: int32(443)}, + }, + }, + }, + }, + }, + port: 443, + containerPort: 443, + err: true, + }, + { + name: "test failure 2 (cannot find a matching service port, but ClusterIP: None)", + svc: v1.Service{ + Spec: v1.ServiceSpec{ + ClusterIP: v1.ClusterIPNone, + Ports: []v1.ServicePort{ + { + Port: 80, + TargetPort: intstr.FromString("http"), + }, + }, + }, + }, + pod: v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Ports: []v1.ContainerPort{ + { + Name: "http", + ContainerPort: int32(80)}, + }, + }, + }, + }, + }, + port: 443, + containerPort: 443, + err: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + containerPort, err := LookupContainerPortNumberByServicePort(tt.svc, tt.pod, tt.port) + if err != nil { + if tt.err { + if containerPort != tt.containerPort { + t.Errorf("%v: expected port %v; got %v", tt.name, tt.containerPort, containerPort) + } + return + } + + t.Errorf("%v: unexpected error: %v", tt.name, err) + return + } + + if tt.err { + t.Errorf("%v: unexpected success", tt.name) + return + } + + if containerPort != tt.containerPort { + t.Errorf("%v: expected port %v; got %v", tt.name, tt.containerPort, containerPort) + } + }) + } +} diff --git a/pkg/util/umask.go b/pkg/util/umask.go new file mode 100644 index 00000000..67add4e9 --- /dev/null +++ b/pkg/util/umask.go @@ -0,0 +1,28 @@ +// +build !windows + +/* +Copyright 2014 The Kubernetes Authors. + +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 util + +import ( + "golang.org/x/sys/unix" +) + +// Umask is a wrapper for `unix.Umask()` on non-Windows platforms +func Umask(mask int) (old int, err error) { + return unix.Umask(mask), nil +} diff --git a/pkg/util/umask_windows.go b/pkg/util/umask_windows.go new file mode 100644 index 00000000..5b4f54bb --- /dev/null +++ b/pkg/util/umask_windows.go @@ -0,0 +1,28 @@ +// +build windows + +/* +Copyright 2014 The Kubernetes Authors. + +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 util + +import ( + "errors" +) + +// Umask returns an error on Windows +func Umask(mask int) (int, error) { + return 0, errors.New("platform and architecture is not supported") +} diff --git a/pkg/util/util.go b/pkg/util/util.go new file mode 100644 index 00000000..0c1973df --- /dev/null +++ b/pkg/util/util.go @@ -0,0 +1,93 @@ +/* +Copyright 2017 The Kubernetes Authors. + +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 util + +import ( + "crypto/md5" + "errors" + "fmt" + "path" + "path/filepath" + "strings" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// ParseRFC3339 parses an RFC3339 date in either RFC3339Nano or RFC3339 format. +func ParseRFC3339(s string, nowFn func() metav1.Time) (metav1.Time, error) { + if t, timeErr := time.Parse(time.RFC3339Nano, s); timeErr == nil { + return metav1.Time{Time: t}, nil + } + t, err := time.Parse(time.RFC3339, s) + if err != nil { + return metav1.Time{}, err + } + return metav1.Time{Time: t}, nil +} + +// HashObject returns the hash of a Object hash by a Codec +func HashObject(obj runtime.Object, codec runtime.Codec) (string, error) { + data, err := runtime.Encode(codec, obj) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", md5.Sum(data)), nil +} + +// ParseFileSource parses the source given. +// +// Acceptable formats include: +// 1. source-path: the basename will become the key name +// 2. source-name=source-path: the source-name will become the key name and +// source-path is the path to the key file. +// +// Key names cannot include '='. +func ParseFileSource(source string) (keyName, filePath string, err error) { + numSeparators := strings.Count(source, "=") + switch { + case numSeparators == 0: + return path.Base(filepath.ToSlash(source)), source, nil + case numSeparators == 1 && strings.HasPrefix(source, "="): + return "", "", fmt.Errorf("key name for file path %v missing", strings.TrimPrefix(source, "=")) + case numSeparators == 1 && strings.HasSuffix(source, "="): + return "", "", fmt.Errorf("file path for key name %v missing", strings.TrimSuffix(source, "=")) + case numSeparators > 1: + return "", "", errors.New("Key names or file paths cannot contain '='") + default: + components := strings.Split(source, "=") + return components[0], components[1], nil + } +} + +// ParseLiteralSource parses the source key=val pair into its component pieces. +// This functionality is distinguished from strings.SplitN(source, "=", 2) since +// it returns an error in the case of empty keys, values, or a missing equals sign. +func ParseLiteralSource(source string) (keyName, value string, err error) { + // leading equal is invalid + if strings.Index(source, "=") == 0 { + return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source) + } + // split after the first equal (so values can have the = character) + items := strings.SplitN(source, "=", 2) + if len(items) != 2 { + return "", "", fmt.Errorf("invalid literal source %v, expected key=value", source) + } + + return items[0], items[1], nil +} diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go new file mode 100644 index 00000000..4a4e647a --- /dev/null +++ b/pkg/util/util_test.go @@ -0,0 +1,202 @@ +/* +Copyright 2016 The Kubernetes Authors. + +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 util + +import "testing" + +func TestParseFileSource(t *testing.T) { + tests := []struct { + name string + input string + key string + filepath string + err bool + }{ + { + name: "success 1", + input: "boo=zoo", + key: "boo", + filepath: "zoo", + err: false, + }, + { + name: "success 2", + input: "boo=/path/to/zoo", + key: "boo", + filepath: "/path/to/zoo", + err: false, + }, + { + name: "success 3", + input: "boo-2=/1/2/3/4/5/zab.txt", + key: "boo-2", + filepath: "/1/2/3/4/5/zab.txt", + err: false, + }, + { + name: "success 4", + input: "boo-=this/seems/weird.txt", + key: "boo-", + filepath: "this/seems/weird.txt", + err: false, + }, + { + name: "success 5", + input: "-key=some/path", + key: "-key", + filepath: "some/path", + err: false, + }, + { + name: "invalid 1", + input: "key==some/path", + err: true, + }, + { + name: "invalid 2", + input: "=key=some/path", + err: true, + }, + { + name: "invalid 3", + input: "==key=/some/other/path", + err: true, + }, + { + name: "invalid 4", + input: "=key", + err: true, + }, + { + name: "invalid 5", + input: "key=", + err: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key, filepath, err := ParseFileSource(tt.input) + if err != nil { + if tt.err { + return + } + + t.Errorf("%v: unexpected error: %v", tt.name, err) + return + } + + if tt.err { + t.Errorf("%v: unexpected success", tt.name) + return + } + + if e, a := tt.key, key; e != a { + t.Errorf("%v: expected key %v; got %v", tt.name, e, a) + return + } + + if e, a := tt.filepath, filepath; e != a { + t.Errorf("%v: expected filepath %v; got %v", tt.name, e, a) + } + }) + } +} + +func TestParseLiteralSource(t *testing.T) { + tests := []struct { + name string + input string + key string + value string + err bool + }{ + { + name: "success 1", + input: "key=value", + key: "key", + value: "value", + err: false, + }, + { + name: "success 2", + input: "key=value/with/slashes", + key: "key", + value: "value/with/slashes", + err: false, + }, + { + name: "err 1", + input: "key==value", + key: "key", + value: "=value", + err: false, + }, + { + name: "err 2", + input: "key=value=", + key: "key", + value: "value=", + err: false, + }, + { + name: "err 3", + input: "key2=value==", + key: "key2", + value: "value==", + err: false, + }, + { + name: "err 4", + input: "==key", + err: true, + }, + { + name: "err 5", + input: "=key=", + err: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key, value, err := ParseLiteralSource(tt.input) + if err != nil { + if tt.err { + return + } + + t.Errorf("%v: unexpected error: %v", tt.name, err) + return + } + + if tt.err { + t.Errorf("%v: unexpected success", tt.name) + return + } + + if e, a := tt.key, key; e != a { + t.Errorf("%v: expected key %v; got %v", tt.name, e, a) + return + } + + if e, a := tt.value, value; e != a { + t.Errorf("%v: expected value %v; got %v", tt.name, e, a) + } + }) + } +}